diff --git a/apps/blog-next/.env.example b/apps/blog-next/.env.example index b44e3b0..d382e1b 100644 --- a/apps/blog-next/.env.example +++ b/apps/blog-next/.env.example @@ -12,7 +12,19 @@ STORAGE_ROUTE_PREFIX= CACHE_PREFIX= +MAIL_MAILER= +MAIL_FROM_ADDRESS= +MAIL_FROM_NAME= +MAIL_LOG_BODIES= +MAIL_HOST= +MAIL_PORT= +MAIL_SECURE= +MAIL_USERNAME= +MAIL_PASSWORD= + AUTH_SOCIAL_ENCRYPTION_KEY= +AUTH_EMAIL_VERIFICATION_ROUTE=/verify-email +AUTH_PASSWORD_RESET_ROUTE=/reset-password SESSION_DRIVER= SESSION_CONNECTION= SESSION_COOKIE= diff --git a/apps/blog-next/app/admin/page.tsx b/apps/blog-next/app/admin/page.tsx index 6ca5783..7ae359d 100644 --- a/apps/blog-next/app/admin/page.tsx +++ b/apps/blog-next/app/admin/page.tsx @@ -1,15 +1,23 @@ import Link from 'next/link' +import { user } from '@holo-js/auth' import { getAdminDashboardData } from '@/server/lib/blog' export const dynamic = 'force-dynamic' export default async function AdminDashboardPage() { - const dashboard = await getAdminDashboardData() + const [dashboard, currentUser] = await Promise.all([ + getAdminDashboardData(), + user(), + ]) + const displayName = currentUser?.name ?? currentUser?.email ?? 'Editor' return (
-

Admin

+
+

Admin

+

Signed in as {displayName}

+
{dashboard.postCount}
Posts
{dashboard.publishedCount}
Published
diff --git a/apps/blog-next/app/api/auth/user/route.ts b/apps/blog-next/app/api/auth/user/route.ts new file mode 100644 index 0000000..1777ac2 --- /dev/null +++ b/apps/blog-next/app/api/auth/user/route.ts @@ -0,0 +1,9 @@ +import { check, user } from '@holo-js/auth' + +export async function GET() { + return Response.json({ + authenticated: await check(), + guard: 'web', + user: await user(), + }) +} diff --git a/apps/blog-next/app/api/forgot-password/route.ts b/apps/blog-next/app/api/forgot-password/route.ts new file mode 100644 index 0000000..001e7d7 --- /dev/null +++ b/apps/blog-next/app/api/forgot-password/route.ts @@ -0,0 +1,31 @@ +import { requestPasswordReset } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { forgotPasswordForm } from '@/lib/schemas/auth' + +export async function POST(request: Request) { + const submission = await validate(request, forgotPasswordForm) + + if (!submission.valid) { + return Response.json(submission.fail(), { + status: submission.fail().status, + }) + } + + const { error } = await requestPasswordReset(submission.data) + if (error) { + return Response.json({ + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + }, { + status: error.status, + }) + } + + return Response.json(submission.success({ + message: 'If an account exists for that email, a reset link has been sent.', + })) +} diff --git a/apps/blog-next/app/api/login/route.ts b/apps/blog-next/app/api/login/route.ts new file mode 100644 index 0000000..85f8ca8 --- /dev/null +++ b/apps/blog-next/app/api/login/route.ts @@ -0,0 +1,46 @@ +import { login } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { loginForm } from '@/lib/schemas/auth' + +export async function POST(request: Request) { + const submission = await validate(request, loginForm, { + throttle: 'login', + }) + + if (!submission.valid) { + return Response.json(submission.fail(), { + status: submission.fail().status, + }) + } + + const { data: session, error } = await login(submission.data) + if (error) { + return Response.json({ + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + }, { + status: error.status, + }) + } + + const headers = new Headers() + for (const cookie of session.cookies) { + headers.append('set-cookie', cookie) + } + + return Response.json(submission.success({ + message: session.emailVerificationRequired + ? 'Signed in. Verify your email address to continue.' + : 'Signed in successfully.', + redirectTo: session.emailVerificationRequired + ? session.emailVerificationRoute ?? '/verify-email' + : '/admin', + user: session.user, + }), { + headers, + }) +} diff --git a/apps/blog-next/app/api/logout/route.ts b/apps/blog-next/app/api/logout/route.ts new file mode 100644 index 0000000..3be87d2 --- /dev/null +++ b/apps/blog-next/app/api/logout/route.ts @@ -0,0 +1,18 @@ +import { logout, user } from '@holo-js/auth' + +export async function POST() { + const signedOut = await logout() + const headers = new Headers() + for (const cookie of signedOut.cookies) { + headers.append('set-cookie', cookie) + } + + return Response.json({ + ok: true, + authenticated: false, + message: 'Signed out successfully.', + user: await user(), + }, { + headers, + }) +} diff --git a/apps/blog-next/app/api/register/route.ts b/apps/blog-next/app/api/register/route.ts new file mode 100644 index 0000000..f7c109c --- /dev/null +++ b/apps/blog-next/app/api/register/route.ts @@ -0,0 +1,48 @@ +import { loginUsing, register } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { registerForm } from '@/lib/schemas/auth' + +export async function POST(request: Request) { + const submission = await validate(request, registerForm, { + throttle: 'register', + }) + + if (!submission.valid) { + return Response.json(submission.fail(), { + status: submission.fail().status, + }) + } + + const { data: created, error } = await register(submission.data) + if (error) { + return Response.json({ + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + }, { + status: error.status, + }) + } + + const session = await loginUsing(created) + const headers = new Headers() + for (const cookie of session.cookies) { + headers.append('set-cookie', cookie) + } + + return Response.json(submission.success({ + message: session.emailVerificationRequired + ? 'Account created. Check your inbox to verify your email address.' + : 'Account created and signed in successfully.', + redirectTo: session.emailVerificationRequired + ? session.emailVerificationRoute ?? '/verify-email' + : '/admin', + user: session.user, + }, 201), { + status: 201, + headers, + }) +} diff --git a/apps/blog-next/app/api/reset-password/route.ts b/apps/blog-next/app/api/reset-password/route.ts new file mode 100644 index 0000000..3cd5e3a --- /dev/null +++ b/apps/blog-next/app/api/reset-password/route.ts @@ -0,0 +1,32 @@ +import { resetPassword } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { resetPasswordForm } from '@/lib/schemas/auth' + +export async function POST(request: Request) { + const submission = await validate(request, resetPasswordForm) + + if (!submission.valid) { + return Response.json(submission.fail(), { + status: submission.fail().status, + }) + } + + const { error } = await resetPassword(submission.data) + if (error) { + return Response.json({ + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + }, { + status: error.status, + }) + } + + return Response.json(submission.success({ + message: 'Password reset successfully. You can sign in with your new password.', + redirectTo: '/login', + })) +} diff --git a/apps/blog-next/app/api/verify-email/resend/route.ts b/apps/blog-next/app/api/verify-email/resend/route.ts new file mode 100644 index 0000000..4edc3ef --- /dev/null +++ b/apps/blog-next/app/api/verify-email/resend/route.ts @@ -0,0 +1,42 @@ +import { verification } from '@holo-js/auth' + +interface ResendVerificationRequestBody { + readonly email?: string +} + +async function readRequestBody(request: Request): Promise { + const contentType = request.headers.get('content-type') ?? '' + if (!contentType.includes('application/json')) { + return {} + } + + const payload = await request.json().catch(() => null) + if (!payload || typeof payload !== 'object') { + return {} + } + + const email = typeof payload.email === 'string' ? payload.email.trim() : undefined + return email ? { email } : {} +} + +export async function POST(request: Request) { + const input = await readRequestBody(request) + const { error } = await verification.resend(input) + if (error) { + return Response.json({ + ok: false as const, + status: error.status, + errors: error.fields, + }, { + status: error.status, + }) + } + + return Response.json({ + ok: true as const, + status: 200, + data: { + message: 'A fresh verification email has been sent.', + }, + }) +} diff --git a/apps/blog-next/app/api/verify-email/route.ts b/apps/blog-next/app/api/verify-email/route.ts new file mode 100644 index 0000000..ab7cf90 --- /dev/null +++ b/apps/blog-next/app/api/verify-email/route.ts @@ -0,0 +1,32 @@ +import { verification } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { verifyEmailForm } from '@/lib/schemas/auth' + +export async function POST(request: Request) { + const submission = await validate(request, verifyEmailForm) + + if (!submission.valid) { + return Response.json(submission.fail(), { + status: submission.fail().status, + }) + } + + const { error } = await verification.consume(submission.data.token) + if (error) { + return Response.json({ + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + }, { + status: error.status, + }) + } + + return Response.json(submission.success({ + message: 'Email address verified. You can sign in now.', + redirectTo: '/login', + })) +} diff --git a/apps/blog-next/app/forgot-password/page.tsx b/apps/blog-next/app/forgot-password/page.tsx new file mode 100644 index 0000000..6aed218 --- /dev/null +++ b/apps/blog-next/app/forgot-password/page.tsx @@ -0,0 +1,59 @@ +'use client' + +import Link from 'next/link' + +import { useForm } from '@holo-js/adapter-next/client' + +import { forgotPasswordForm } from '@/lib/schemas/auth' + +const panelStyle = { + display: 'grid', + gap: '1rem', + maxWidth: '32rem', + padding: '1.5rem', + borderRadius: '1rem', + background: '#111827', + border: '1px solid rgba(148, 163, 184, 0.16)', +} satisfies React.CSSProperties + +export default function ForgotPasswordPage() { + const form = useForm(forgotPasswordForm, { + validateOn: 'blur', + initialValues: { email: '' }, + async submitter({ formData }) { + const response = await fetch('/api/forgot-password', { method: 'POST', body: formData }) + return await response.json() + }, + }) + + return ( +
+
+

Forgot password

+

Request a password reset link for your local account.

+
+ +
{ event.preventDefault(); form.submit() }} style={{ display: 'grid', gap: '0.9rem' }}> + + + +
+ + {form.lastSubmission?.ok === true ?

A password reset link has been sent if the account exists.

: null} + + Back to sign in +
+ ) +} diff --git a/apps/blog-next/app/layout.tsx b/apps/blog-next/app/layout.tsx index dc9d6b6..76a8f90 100644 --- a/apps/blog-next/app/layout.tsx +++ b/apps/blog-next/app/layout.tsx @@ -17,6 +17,8 @@ export default function RootLayout({ children }: { children: ReactNode }) { blog-next Posts Admin + Login + Register
{children}
diff --git a/apps/blog-next/app/login/page.tsx b/apps/blog-next/app/login/page.tsx new file mode 100644 index 0000000..ebb52b5 --- /dev/null +++ b/apps/blog-next/app/login/page.tsx @@ -0,0 +1,93 @@ +'use client' + +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { useForm } from '@holo-js/adapter-next/client' +import { loginForm } from '@/lib/schemas/auth' + +const panelStyle = { + display: 'grid', + gap: '1rem', + maxWidth: '32rem', + padding: '1.5rem', + borderRadius: '1rem', + background: '#111827', + border: '1px solid rgba(148, 163, 184, 0.16)', +} satisfies React.CSSProperties + +export default function LoginPage() { + const router = useRouter() + const form = useForm(loginForm, { + validateOn: 'blur', + initialValues: { email: '', password: '', remember: false }, + async submitter({ formData }) { + const response = await fetch('/api/login', { method: 'POST', body: formData }) + const submission = await response.json() + if (submission?.ok === true && typeof submission.data?.redirectTo === 'string') { + router.replace(submission.data.redirectTo) + } + return submission + }, + }) + + return ( +
+
+

Sign in

+

Use your email address and password to access the admin area.

+
+ +
{ event.preventDefault(); form.submit() }} style={{ display: 'grid', gap: '0.9rem' }}> + + + + + + + +
+ + {form.lastSubmission?.ok === true ? ( +
+

Signed in successfully.

+ Continue to admin +
+ ) : null} + +
+ Create account + Forgot password? +
+
+ ) +} diff --git a/apps/blog-next/app/register/page.tsx b/apps/blog-next/app/register/page.tsx new file mode 100644 index 0000000..ecdd5f6 --- /dev/null +++ b/apps/blog-next/app/register/page.tsx @@ -0,0 +1,105 @@ +'use client' + +import Link from 'next/link' +import { useRouter } from 'next/navigation' + +import { useForm } from '@holo-js/adapter-next/client' + +import { registerForm } from '@/lib/schemas/auth' + +const panelStyle = { + display: 'grid', + gap: '1rem', + maxWidth: '36rem', + padding: '1.5rem', + borderRadius: '1rem', + background: '#111827', + border: '1px solid rgba(148, 163, 184, 0.16)', +} satisfies React.CSSProperties + +export default function RegisterPage() { + const router = useRouter() + const form = useForm(registerForm, { + validateOn: 'blur', + initialValues: { name: '', email: '', password: '', passwordConfirmation: '' }, + async submitter({ formData }) { + const response = await fetch('/api/register', { method: 'POST', body: formData }) + const submission = await response.json() + if (submission?.ok === true && typeof submission.data?.redirectTo === 'string') { + router.replace(submission.data.redirectTo) + } + return submission + }, + }) + + return ( +
+
+

Create account

+

Create a local user account and verify the email address before signing in.

+
+ +
{ event.preventDefault(); form.submit() }} style={{ display: 'grid', gap: '0.9rem' }}> + + + + + + + + + +
+ + {form.lastSubmission?.ok === true ? ( +
+

Account created. Check your inbox to verify your email address.

+ Return to sign in +
+ ) : null} + + Already have an account? +
+ ) +} diff --git a/apps/blog-next/app/reset-password/page.tsx b/apps/blog-next/app/reset-password/page.tsx new file mode 100644 index 0000000..e71ee2e --- /dev/null +++ b/apps/blog-next/app/reset-password/page.tsx @@ -0,0 +1,100 @@ +'use client' + +import { Suspense } from 'react' +import Link from 'next/link' +import { useRouter, useSearchParams } from 'next/navigation' + +import { useForm } from '@holo-js/adapter-next/client' + +import { resetPasswordForm } from '@/lib/schemas/auth' + +const panelStyle = { + display: 'grid', + gap: '1rem', + maxWidth: '36rem', + padding: '1.5rem', + borderRadius: '1rem', + background: '#111827', + border: '1px solid rgba(148, 163, 184, 0.16)', +} satisfies React.CSSProperties + +function ResetPasswordPageContent() { + const router = useRouter() + const searchParams = useSearchParams() + const token = searchParams.get('token') ?? '' + + const form = useForm(resetPasswordForm, { + validateOn: 'blur', + initialValues: { token, password: '', passwordConfirmation: '' }, + async submitter({ formData }) { + const response = await fetch('/api/reset-password', { method: 'POST', body: formData }) + const submission = await response.json() + if (submission?.ok === true && typeof submission.data?.redirectTo === 'string') { + router.replace(submission.data.redirectTo) + } + return submission + }, + }) + + return ( +
+
+

Reset password

+

Set a new password using the reset link from your email.

+
+ + {token.length > 0 ? ( +
{ event.preventDefault(); form.submit() }} style={{ display: 'grid', gap: '0.9rem' }}> + + + + + + + {form.errors.has('token') ? {form.errors.first('token')} : null} + + +
+ ) : ( +

A reset token is required to complete this form.

+ )} + + {form.lastSubmission?.ok === true ? ( +
+

Your password has been reset successfully.

+ Sign in +
+ ) : null} +
+ ) +} + +export default function ResetPasswordPage() { + return ( +

Loading reset form…

}> + + + ) +} diff --git a/apps/blog-next/app/verify-email/page.tsx b/apps/blog-next/app/verify-email/page.tsx new file mode 100644 index 0000000..2ead28f --- /dev/null +++ b/apps/blog-next/app/verify-email/page.tsx @@ -0,0 +1,125 @@ +'use client' + +import { Suspense } from 'react' +import { useState } from 'react' +import Link from 'next/link' +import { useRouter, useSearchParams } from 'next/navigation' + +import { useForm } from '@holo-js/adapter-next/client' + +import { verifyEmailForm } from '@/lib/schemas/auth' + +const panelStyle = { + display: 'grid', + gap: '1rem', + maxWidth: '36rem', + padding: '1.5rem', + borderRadius: '1rem', + background: '#111827', + border: '1px solid rgba(148, 163, 184, 0.16)', +} satisfies React.CSSProperties + +function VerifyEmailPageContent() { + const router = useRouter() + const searchParams = useSearchParams() + const token = searchParams.get('token') ?? '' + const email = searchParams.get('email') ?? '' + const [resendMessage, setResendMessage] = useState('') + const [resendError, setResendError] = useState('') + const [resending, setResending] = useState(false) + + const form = useForm(verifyEmailForm, { + initialValues: { token }, + async submitter({ formData }) { + const response = await fetch('/api/verify-email', { method: 'POST', body: formData }) + const submission = await response.json() + if (submission?.ok === true && typeof submission.data?.redirectTo === 'string') { + router.replace(submission.data.redirectTo) + } + return submission + }, + }) + + async function resendVerificationEmail() { + setResending(true) + setResendError('') + setResendMessage('') + + try { + const response = await fetch('/api/verify-email/resend', { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify(email ? { email } : {}), + }) + const payload = await response.json() + if (payload?.ok === true) { + setResendMessage(payload.data?.message ?? 'A fresh verification email has been sent.') + return + } + + const message = Array.isArray(payload?.errors?._root) + ? payload.errors._root[0] + : 'Could not send another verification email.' + setResendError(typeof message === 'string' ? message : 'Could not send another verification email.') + } catch (error) { + console.error('Failed to resend verification email.', error) + setResendError('Could not send another verification email.') + } finally { + setResending(false) + } + } + + return ( +
+
+

Verify your email

+

Use the verification link from your inbox to confirm the account.

+
+ + {token.length > 0 ? ( +
{ event.preventDefault(); form.submit() }} style={{ display: 'grid', gap: '0.9rem' }}> + + {form.errors.has('token') ? {form.errors.first('token')} : null} + +
+ ) : ( +
+

+ {email + ? `Check ${email} for the verification email, then open the link from this page.` + : 'Check your inbox for the verification email, then open the link from this page.'} +

+ + {resendMessage ?

{resendMessage}

: null} + {resendError ?

{resendError}

: null} +
+ )} + + {form.lastSubmission?.ok === true ? ( +
+

Your email address has been verified.

+ Sign in +
+ ) : null} + +
+ Create another account + Back to sign in +
+
+ ) +} + +export default function VerifyEmailPage() { + return ( +

Loading verification form…

}> + +
+ ) +} diff --git a/apps/blog-next/config/auth.ts b/apps/blog-next/config/auth.ts index 2b9e525..2c9f962 100644 --- a/apps/blog-next/config/auth.ts +++ b/apps/blog-next/config/auth.ts @@ -31,10 +31,12 @@ export default defineAuthConfig({ table: 'password_reset_tokens', expire: 60, throttle: 60, + route: env('AUTH_PASSWORD_RESET_ROUTE', '/reset-password'), }, }, emailVerification: { - required: false, + required: true, + route: env('AUTH_EMAIL_VERIFICATION_ROUTE', '/verify-email'), }, personalAccessTokens: { defaultAbilities: [], diff --git a/apps/blog-next/config/mail.ts b/apps/blog-next/config/mail.ts index 6cf622f..8556fe2 100644 --- a/apps/blog-next/config/mail.ts +++ b/apps/blog-next/config/mail.ts @@ -15,6 +15,7 @@ export default defineMailConfig({ }, log: { driver: 'log', + logBodies: env('MAIL_LOG_BODIES', false), }, fake: { driver: 'fake', @@ -23,7 +24,9 @@ export default defineMailConfig({ driver: 'smtp', host: env('MAIL_HOST', '127.0.0.1'), port: env('MAIL_PORT', 1025), - secure: env('MAIL_SECURE', false), + secure: env('MAIL_SECURE', false), + user: env('MAIL_USERNAME') || undefined, + password: env('MAIL_PASSWORD') || undefined, }, }, }) diff --git a/apps/blog-next/config/session.ts b/apps/blog-next/config/session.ts index 176dd27..57125de 100644 --- a/apps/blog-next/config/session.ts +++ b/apps/blog-next/config/session.ts @@ -1,5 +1,15 @@ import { defineSessionConfig, env } from '@holo-js/config' +const configuredSessionSameSite = env('SESSION_SAME_SITE') +const sessionSameSite = configuredSessionSameSite === 'strict' + || configuredSessionSameSite === 'lax' + || configuredSessionSameSite === 'none' + ? configuredSessionSameSite + : 'lax' +const sessionSecure = sessionSameSite === 'none' + ? true + : env('SESSION_SECURE', false) + export default defineSessionConfig({ driver: env('SESSION_DRIVER', 'file'), stores: { @@ -17,9 +27,9 @@ export default defineSessionConfig({ name: env('SESSION_COOKIE', 'holo_session'), path: env('SESSION_PATH', '/'), domain: env('SESSION_DOMAIN'), - secure: env('SESSION_SECURE', false), + secure: sessionSecure, httpOnly: true, - sameSite: env<'lax' | 'strict' | 'none'>('SESSION_SAME_SITE', 'lax'), + sameSite: sessionSameSite, }, idleTimeout: env('SESSION_IDLE_TIMEOUT', 120), absoluteLifetime: env('SESSION_LIFETIME', 120), diff --git a/apps/blog-next/lib/schemas/auth.ts b/apps/blog-next/lib/schemas/auth.ts new file mode 100644 index 0000000..0f7a314 --- /dev/null +++ b/apps/blog-next/lib/schemas/auth.ts @@ -0,0 +1,28 @@ +import { field, schema } from '@holo-js/forms/schema' + +export const loginForm = schema({ + email: field.string().required('Email is required.').email('Enter a valid email address.'), + password: field.string().required('Password is required.').min(8, 'Password must be at least 8 characters.'), + remember: field.boolean().default(false), +}) + +export const registerForm = schema({ + name: field.string().required('Name is required.').min(3, 'Name must be at least 3 characters.'), + email: field.string().required('Email is required.').email('Enter a valid email address.'), + password: field.string().required('Password is required.').min(8, 'Password must be at least 8 characters.').confirmed(), + passwordConfirmation: field.string().required('Please confirm your password.'), +}) + +export const forgotPasswordForm = schema({ + email: field.string().required('Email is required.').email('Enter a valid email address.'), +}) + +export const resetPasswordForm = schema({ + token: field.string().required('Reset token is required.'), + password: field.string().required('Password is required.').min(8, 'Password must be at least 8 characters.').confirmed(), + passwordConfirmation: field.string().required('Please confirm your password.'), +}) + +export const verifyEmailForm = schema({ + token: field.string().required('Verification token is required.'), +}) diff --git a/apps/blog-next/package.json b/apps/blog-next/package.json index ea47ae3..9ec3488 100644 --- a/apps/blog-next/package.json +++ b/apps/blog-next/package.json @@ -45,7 +45,7 @@ "@holo-js/storage": "^0.1.4", "@holo-js/validation": "workspace:*", "esbuild": "^0.25.0", - "next": "^16.0.0", + "next": "catalog:", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/apps/blog-next/tests/run.mjs b/apps/blog-next/tests/run.mjs index f3f8f7f..6762d11 100644 --- a/apps/blog-next/tests/run.mjs +++ b/apps/blog-next/tests/run.mjs @@ -6,6 +6,7 @@ import { get } from 'node:http' import { createServer } from 'node:net' import { join } from 'node:path' import { pathToFileURL } from 'node:url' +import { assertExampleAppAuthFlow } from '../../../tests/example-app-auth-flow.mjs' const cwd = process.cwd() const configPath = join(cwd, 'config/app.ts') @@ -33,10 +34,12 @@ const port = await new Promise((resolve, reject) => { const healthUrl = `http://localhost:${port}/api/holo/health` const originalConfig = await readFile(configPath, 'utf8') const runtimeSchemaPath = join(cwd, '.holo-js/generated/schema.mjs') +let capturedOutput = '' function createChildEnv(overrides = {}) { const env = { ...process.env, + HOLO_SECURITY_TRUST_PROXY: 'true', ...overrides, } @@ -150,6 +153,18 @@ async function waitForText(url, predicate, timeoutMs = 30000) { throw new Error(`Timed out waiting for ${url}${lastError instanceof Error ? `: ${lastError.message}` : ''}`) } +function pipeOutput(stream, target) { + if (!stream) { + return + } + + stream.on('data', chunk => { + const text = chunk.toString() + capturedOutput += text + target.write(text) + }) +} + let child = null function killChildTree() { @@ -181,14 +196,23 @@ try { PORT: port, HOST: 'localhost', APP_URL: `http://localhost:${port}`, + MAIL_MAILER: 'log', + MAIL_LOG_BODIES: 'true', }), - stdio: 'inherit', + stdio: ['inherit', 'pipe', 'pipe'], }) + pipeOutput(child.stdout, process.stdout) + pipeOutput(child.stderr, process.stderr) const initial = await waitForJson(healthUrl, payload => payload.ok === true) assert.equal(initial.app, 'blog-next') await waitForText(`http://localhost:${port}/`, payload => payload.includes('Shipping a Real Holo Blog on Next')) await waitForText(`http://localhost:${port}/admin/posts`, payload => payload.includes('Designing the Example App Roadmap')) + await assertExampleAppAuthFlow({ + baseUrl: `http://localhost:${port}`, + getOutput: () => capturedOutput, + appName: 'blog-next', + }) await writeFile(configPath, originalConfig.replace("name: env('APP_NAME', 'blog-next')", "name: env('APP_NAME', 'blog-next-updated')")) const updated = await waitForJson(healthUrl, payload => payload.app === 'blog-next-updated') diff --git a/apps/blog-next/tsconfig.json b/apps/blog-next/tsconfig.json index c242608..0e8777f 100644 --- a/apps/blog-next/tsconfig.json +++ b/apps/blog-next/tsconfig.json @@ -37,6 +37,7 @@ "instrumentation.ts", "app/**/*.ts", "app/**/*.tsx", + "lib/**/*.ts", "server/**/*.ts", "config/**/*.ts", ".holo-js/generated/**/*.ts", diff --git a/apps/blog-nuxt/.env.example b/apps/blog-nuxt/.env.example index b44e3b0..d382e1b 100644 --- a/apps/blog-nuxt/.env.example +++ b/apps/blog-nuxt/.env.example @@ -12,7 +12,19 @@ STORAGE_ROUTE_PREFIX= CACHE_PREFIX= +MAIL_MAILER= +MAIL_FROM_ADDRESS= +MAIL_FROM_NAME= +MAIL_LOG_BODIES= +MAIL_HOST= +MAIL_PORT= +MAIL_SECURE= +MAIL_USERNAME= +MAIL_PASSWORD= + AUTH_SOCIAL_ENCRYPTION_KEY= +AUTH_EMAIL_VERIFICATION_ROUTE=/verify-email +AUTH_PASSWORD_RESET_ROUTE=/reset-password SESSION_DRIVER= SESSION_CONNECTION= SESSION_COOKIE= diff --git a/apps/blog-nuxt/app.vue b/apps/blog-nuxt/app.vue index 9359ba0..d0ebf60 100644 --- a/apps/blog-nuxt/app.vue +++ b/apps/blog-nuxt/app.vue @@ -5,6 +5,8 @@ blog-nuxt Posts Admin + Login + Register
diff --git a/apps/blog-nuxt/config/auth.ts b/apps/blog-nuxt/config/auth.ts index 2b9e525..2c9f962 100644 --- a/apps/blog-nuxt/config/auth.ts +++ b/apps/blog-nuxt/config/auth.ts @@ -31,10 +31,12 @@ export default defineAuthConfig({ table: 'password_reset_tokens', expire: 60, throttle: 60, + route: env('AUTH_PASSWORD_RESET_ROUTE', '/reset-password'), }, }, emailVerification: { - required: false, + required: true, + route: env('AUTH_EMAIL_VERIFICATION_ROUTE', '/verify-email'), }, personalAccessTokens: { defaultAbilities: [], diff --git a/apps/blog-nuxt/config/mail.ts b/apps/blog-nuxt/config/mail.ts index 304d35c..8556fe2 100644 --- a/apps/blog-nuxt/config/mail.ts +++ b/apps/blog-nuxt/config/mail.ts @@ -15,6 +15,7 @@ export default defineMailConfig({ }, log: { driver: 'log', + logBodies: env('MAIL_LOG_BODIES', false), }, fake: { driver: 'fake', @@ -24,6 +25,8 @@ export default defineMailConfig({ host: env('MAIL_HOST', '127.0.0.1'), port: env('MAIL_PORT', 1025), secure: env('MAIL_SECURE', false), + user: env('MAIL_USERNAME') || undefined, + password: env('MAIL_PASSWORD') || undefined, }, }, }) diff --git a/apps/blog-nuxt/config/session.ts b/apps/blog-nuxt/config/session.ts index 843b169..57125de 100644 --- a/apps/blog-nuxt/config/session.ts +++ b/apps/blog-nuxt/config/session.ts @@ -1,10 +1,14 @@ import { defineSessionConfig, env } from '@holo-js/config' -const sessionSameSite = env('SESSION_SAME_SITE') === 'strict' - ? 'strict' - : env('SESSION_SAME_SITE') === 'none' - ? 'none' - : 'lax' +const configuredSessionSameSite = env('SESSION_SAME_SITE') +const sessionSameSite = configuredSessionSameSite === 'strict' + || configuredSessionSameSite === 'lax' + || configuredSessionSameSite === 'none' + ? configuredSessionSameSite + : 'lax' +const sessionSecure = sessionSameSite === 'none' + ? true + : env('SESSION_SECURE', false) export default defineSessionConfig({ driver: env('SESSION_DRIVER', 'file'), @@ -23,7 +27,7 @@ export default defineSessionConfig({ name: env('SESSION_COOKIE', 'holo_session'), path: env('SESSION_PATH', '/'), domain: env('SESSION_DOMAIN'), - secure: env('SESSION_SECURE', false), + secure: sessionSecure, httpOnly: true, sameSite: sessionSameSite, }, diff --git a/apps/blog-nuxt/lib/schemas/auth.ts b/apps/blog-nuxt/lib/schemas/auth.ts new file mode 100644 index 0000000..0f7a314 --- /dev/null +++ b/apps/blog-nuxt/lib/schemas/auth.ts @@ -0,0 +1,28 @@ +import { field, schema } from '@holo-js/forms/schema' + +export const loginForm = schema({ + email: field.string().required('Email is required.').email('Enter a valid email address.'), + password: field.string().required('Password is required.').min(8, 'Password must be at least 8 characters.'), + remember: field.boolean().default(false), +}) + +export const registerForm = schema({ + name: field.string().required('Name is required.').min(3, 'Name must be at least 3 characters.'), + email: field.string().required('Email is required.').email('Enter a valid email address.'), + password: field.string().required('Password is required.').min(8, 'Password must be at least 8 characters.').confirmed(), + passwordConfirmation: field.string().required('Please confirm your password.'), +}) + +export const forgotPasswordForm = schema({ + email: field.string().required('Email is required.').email('Enter a valid email address.'), +}) + +export const resetPasswordForm = schema({ + token: field.string().required('Reset token is required.'), + password: field.string().required('Password is required.').min(8, 'Password must be at least 8 characters.').confirmed(), + passwordConfirmation: field.string().required('Please confirm your password.'), +}) + +export const verifyEmailForm = schema({ + token: field.string().required('Verification token is required.'), +}) diff --git a/apps/blog-nuxt/nuxt.config.ts b/apps/blog-nuxt/nuxt.config.ts index e0208a7..e2a912d 100644 --- a/apps/blog-nuxt/nuxt.config.ts +++ b/apps/blog-nuxt/nuxt.config.ts @@ -1,23 +1,3 @@ export default defineNuxtConfig({ modules: ['@holo-js/adapter-nuxt'], - sourcemap: { - client: false, - server: false, - }, - vite: { - build: { - rollupOptions: { - onwarn(warning, defaultHandler) { - if ( - warning.message.includes('nuxt:module-preload-polyfill') - && warning.message.includes('didn\'t generate a sourcemap') - ) { - return - } - - defaultHandler(warning) - }, - }, - }, - }, }) diff --git a/apps/blog-nuxt/package.json b/apps/blog-nuxt/package.json index 68e8615..d5fcc68 100644 --- a/apps/blog-nuxt/package.json +++ b/apps/blog-nuxt/package.json @@ -46,14 +46,13 @@ "@holo-js/storage": "^0.1.4", "@holo-js/validation": "workspace:*", "esbuild": "^0.25.0", - "nuxt": "^4.0.0", + "nuxt": "catalog:", "vue": "^3.5.13", "vue-router": "^5.0.4" }, "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.8.0", - "vite": "^5.4.14", "vue-tsc": "^2.2.0" } } diff --git a/apps/blog-nuxt/pages/forgot-password.vue b/apps/blog-nuxt/pages/forgot-password.vue new file mode 100644 index 0000000..bbced53 --- /dev/null +++ b/apps/blog-nuxt/pages/forgot-password.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/apps/blog-nuxt/pages/login.vue b/apps/blog-nuxt/pages/login.vue new file mode 100644 index 0000000..d62ff83 --- /dev/null +++ b/apps/blog-nuxt/pages/login.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/apps/blog-nuxt/pages/register.vue b/apps/blog-nuxt/pages/register.vue new file mode 100644 index 0000000..ea71505 --- /dev/null +++ b/apps/blog-nuxt/pages/register.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/apps/blog-nuxt/pages/reset-password.vue b/apps/blog-nuxt/pages/reset-password.vue new file mode 100644 index 0000000..d70453f --- /dev/null +++ b/apps/blog-nuxt/pages/reset-password.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/apps/blog-nuxt/pages/verify-email.vue b/apps/blog-nuxt/pages/verify-email.vue new file mode 100644 index 0000000..f7822dd --- /dev/null +++ b/apps/blog-nuxt/pages/verify-email.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/apps/blog-nuxt/server/api/auth/user.get.ts b/apps/blog-nuxt/server/api/auth/user.get.ts new file mode 100644 index 0000000..412b22f --- /dev/null +++ b/apps/blog-nuxt/server/api/auth/user.get.ts @@ -0,0 +1,9 @@ +import { check, user } from '@holo-js/auth' + +export default defineEventHandler(async () => { + return { + authenticated: await check(), + guard: 'web', + user: await user(), + } +}) diff --git a/apps/blog-nuxt/server/api/forgot-password.post.ts b/apps/blog-nuxt/server/api/forgot-password.post.ts new file mode 100644 index 0000000..c15069a --- /dev/null +++ b/apps/blog-nuxt/server/api/forgot-password.post.ts @@ -0,0 +1,30 @@ +import { requestPasswordReset } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { forgotPasswordForm } from '../../lib/schemas/auth' + +export default defineEventHandler(async (event) => { + const submission = await validate(event, forgotPasswordForm) + + if (!submission.valid) { + const failure = submission.fail() + setResponseStatus(event, failure.status) + return failure + } + + const { error } = await requestPasswordReset(submission.data) + if (error) { + setResponseStatus(event, error.status) + return { + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + } + } + + return submission.success({ + message: 'If an account exists for that email, a reset link has been sent.', + }) +}) diff --git a/apps/blog-nuxt/server/api/login.post.ts b/apps/blog-nuxt/server/api/login.post.ts new file mode 100644 index 0000000..9f51950 --- /dev/null +++ b/apps/blog-nuxt/server/api/login.post.ts @@ -0,0 +1,40 @@ +import { login } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { loginForm } from '../../lib/schemas/auth' + +export default defineEventHandler(async (event) => { + const submission = await validate(event, loginForm, { + throttle: 'login', + }) + + if (!submission.valid) { + const failure = submission.fail() + setResponseStatus(event, failure.status) + return failure + } + + const { data: session, error } = await login(submission.data) + if (error) { + setResponseStatus(event, error.status) + return { + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + } + } + + event.node.res.setHeader('set-cookie', [...session.cookies]) + + return submission.success({ + message: session.emailVerificationRequired + ? 'Signed in. Verify your email address to continue.' + : 'Signed in successfully.', + redirectTo: session.emailVerificationRequired + ? session.emailVerificationRoute ?? '/verify-email' + : '/admin', + user: session.user, + }) +}) diff --git a/apps/blog-nuxt/server/api/logout.post.ts b/apps/blog-nuxt/server/api/logout.post.ts new file mode 100644 index 0000000..aed9bf5 --- /dev/null +++ b/apps/blog-nuxt/server/api/logout.post.ts @@ -0,0 +1,13 @@ +import { logout, user } from '@holo-js/auth' + +export default defineEventHandler(async (event) => { + const signedOut = await logout() + event.node.res.setHeader('set-cookie', [...signedOut.cookies]) + + return { + ok: true, + authenticated: false, + message: 'Signed out successfully.', + user: await user(), + } +}) diff --git a/apps/blog-nuxt/server/api/register.post.ts b/apps/blog-nuxt/server/api/register.post.ts new file mode 100644 index 0000000..0958a55 --- /dev/null +++ b/apps/blog-nuxt/server/api/register.post.ts @@ -0,0 +1,41 @@ +import { loginUsing, register } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { registerForm } from '../../lib/schemas/auth' + +export default defineEventHandler(async (event) => { + const submission = await validate(event, registerForm, { + throttle: 'register', + }) + + if (!submission.valid) { + const failure = submission.fail() + setResponseStatus(event, failure.status) + return failure + } + + const { data: created, error } = await register(submission.data) + if (error) { + setResponseStatus(event, error.status) + return { + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + } + } + + const session = await loginUsing(created) + event.node.res.setHeader('set-cookie', [...session.cookies]) + setResponseStatus(event, 201) + return submission.success({ + message: session.emailVerificationRequired + ? 'Account created. Check your inbox to verify your email address.' + : 'Account created and signed in successfully.', + redirectTo: session.emailVerificationRequired + ? session.emailVerificationRoute ?? '/verify-email' + : '/admin', + user: session.user, + }, 201) +}) diff --git a/apps/blog-nuxt/server/api/reset-password.post.ts b/apps/blog-nuxt/server/api/reset-password.post.ts new file mode 100644 index 0000000..c1ab3d0 --- /dev/null +++ b/apps/blog-nuxt/server/api/reset-password.post.ts @@ -0,0 +1,31 @@ +import { resetPassword } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { resetPasswordForm } from '../../lib/schemas/auth' + +export default defineEventHandler(async (event) => { + const submission = await validate(event, resetPasswordForm) + + if (!submission.valid) { + const failure = submission.fail() + setResponseStatus(event, failure.status) + return failure + } + + const { error } = await resetPassword(submission.data) + if (error) { + setResponseStatus(event, error.status) + return { + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + } + } + + return submission.success({ + message: 'Password reset successfully. You can sign in with your new password.', + redirectTo: '/login', + }) +}) diff --git a/apps/blog-nuxt/server/api/verify-email.post.ts b/apps/blog-nuxt/server/api/verify-email.post.ts new file mode 100644 index 0000000..b1f6ba6 --- /dev/null +++ b/apps/blog-nuxt/server/api/verify-email.post.ts @@ -0,0 +1,31 @@ +import { verification } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { verifyEmailForm } from '../../lib/schemas/auth' + +export default defineEventHandler(async (event) => { + const submission = await validate(event, verifyEmailForm) + + if (!submission.valid) { + const failure = submission.fail() + setResponseStatus(event, failure.status) + return failure + } + + const { error } = await verification.consume(submission.data.token) + if (error) { + setResponseStatus(event, error.status) + return { + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + } + } + + return submission.success({ + message: 'Email address verified. You can sign in now.', + redirectTo: '/login', + }) +}) diff --git a/apps/blog-nuxt/server/api/verify-email/resend.post.ts b/apps/blog-nuxt/server/api/verify-email/resend.post.ts new file mode 100644 index 0000000..790cd8d --- /dev/null +++ b/apps/blog-nuxt/server/api/verify-email/resend.post.ts @@ -0,0 +1,36 @@ +import { verification } from '@holo-js/auth' + +interface ResendVerificationRequestBody { + readonly email?: string +} + +export default defineEventHandler(async (event) => { + let payload: ResendVerificationRequestBody | null = {} + try { + payload = await readBody(event) + } catch { + payload = {} + } + const email = typeof payload === 'object' + && payload !== null + && typeof payload.email === 'string' + ? payload.email.trim() + : '' + const { error } = await verification.resend(email ? { email } : undefined) + if (error) { + setResponseStatus(event, error.status) + return { + ok: false as const, + status: error.status, + errors: error.fields, + } + } + + return { + ok: true as const, + status: 200, + data: { + message: 'A fresh verification email has been sent.', + }, + } +}) diff --git a/apps/blog-nuxt/tests/run.mjs b/apps/blog-nuxt/tests/run.mjs index 25861f9..3116fc7 100644 --- a/apps/blog-nuxt/tests/run.mjs +++ b/apps/blog-nuxt/tests/run.mjs @@ -6,6 +6,7 @@ import { get } from 'node:http' import { createServer } from 'node:net' import { join } from 'node:path' import { pathToFileURL } from 'node:url' +import { assertExampleAppAuthFlow } from '../../../tests/example-app-auth-flow.mjs' const cwd = process.cwd() const configPath = join(cwd, 'config/app.ts') @@ -33,11 +34,15 @@ const port = await new Promise((resolve, reject) => { }) const healthUrl = `http://localhost:${port}/api/holo/health` const originalConfig = await readFile(configPath, 'utf8') +const mirrorCapturedOutput = process.env.MAIL_LOG_VERBOSE === 'true' + || process.argv.includes('--mail-log-verbose') const runtimeSchemaPath = join(cwd, '.holo-js/generated/schema.mjs') +let capturedOutput = '' function createChildEnv(overrides = {}) { const env = { ...process.env, + HOLO_SECURITY_TRUST_PROXY: 'true', ...overrides, } @@ -151,6 +156,20 @@ async function waitForText(url, predicate, timeoutMs = 30000) { throw new Error(`Timed out waiting for ${url}${lastError instanceof Error ? `: ${lastError.message}` : ''}`) } +function pipeOutput(stream, target) { + if (!stream) { + return + } + + stream.on('data', chunk => { + const text = chunk.toString() + capturedOutput += text + if (mirrorCapturedOutput) { + target.write(text) + } + }) +} + let child = null function killChildTree() { @@ -183,14 +202,23 @@ try { HOST: 'localhost', NITRO_HOST: 'localhost', APP_URL: `http://localhost:${port}`, + MAIL_MAILER: 'log', + MAIL_LOG_BODIES: 'true', }), - stdio: 'inherit', + stdio: ['inherit', 'pipe', 'pipe'], }) + pipeOutput(child.stdout, process.stdout) + pipeOutput(child.stderr, process.stderr) const initial = await waitForJson(healthUrl, payload => payload.ok === true) assert.equal(initial.app, 'blog-nuxt') await waitForText(`http://localhost:${port}/`, payload => payload.includes('Shipping a Real Holo Blog on Nuxt')) await waitForText(`http://localhost:${port}/admin/posts`, payload => payload.includes('Designing the Example App Roadmap')) + await assertExampleAppAuthFlow({ + baseUrl: `http://localhost:${port}`, + getOutput: () => capturedOutput, + appName: 'blog-nuxt', + }) await writeFile(configPath, originalConfig.replace("name: env('APP_NAME', 'blog-nuxt')", "name: env('APP_NAME', 'blog-nuxt-updated')")) const updated = await waitForJson(healthUrl, payload => payload.app === 'blog-nuxt-updated') diff --git a/apps/blog-sveltekit/.env.example b/apps/blog-sveltekit/.env.example index b44e3b0..d382e1b 100644 --- a/apps/blog-sveltekit/.env.example +++ b/apps/blog-sveltekit/.env.example @@ -12,7 +12,19 @@ STORAGE_ROUTE_PREFIX= CACHE_PREFIX= +MAIL_MAILER= +MAIL_FROM_ADDRESS= +MAIL_FROM_NAME= +MAIL_LOG_BODIES= +MAIL_HOST= +MAIL_PORT= +MAIL_SECURE= +MAIL_USERNAME= +MAIL_PASSWORD= + AUTH_SOCIAL_ENCRYPTION_KEY= +AUTH_EMAIL_VERIFICATION_ROUTE=/verify-email +AUTH_PASSWORD_RESET_ROUTE=/reset-password SESSION_DRIVER= SESSION_CONNECTION= SESSION_COOKIE= diff --git a/apps/blog-sveltekit/config/auth.ts b/apps/blog-sveltekit/config/auth.ts index 2b9e525..2c9f962 100644 --- a/apps/blog-sveltekit/config/auth.ts +++ b/apps/blog-sveltekit/config/auth.ts @@ -31,10 +31,12 @@ export default defineAuthConfig({ table: 'password_reset_tokens', expire: 60, throttle: 60, + route: env('AUTH_PASSWORD_RESET_ROUTE', '/reset-password'), }, }, emailVerification: { - required: false, + required: true, + route: env('AUTH_EMAIL_VERIFICATION_ROUTE', '/verify-email'), }, personalAccessTokens: { defaultAbilities: [], diff --git a/apps/blog-sveltekit/config/mail.ts b/apps/blog-sveltekit/config/mail.ts index 6cf622f..8556fe2 100644 --- a/apps/blog-sveltekit/config/mail.ts +++ b/apps/blog-sveltekit/config/mail.ts @@ -15,6 +15,7 @@ export default defineMailConfig({ }, log: { driver: 'log', + logBodies: env('MAIL_LOG_BODIES', false), }, fake: { driver: 'fake', @@ -23,7 +24,9 @@ export default defineMailConfig({ driver: 'smtp', host: env('MAIL_HOST', '127.0.0.1'), port: env('MAIL_PORT', 1025), - secure: env('MAIL_SECURE', false), + secure: env('MAIL_SECURE', false), + user: env('MAIL_USERNAME') || undefined, + password: env('MAIL_PASSWORD') || undefined, }, }, }) diff --git a/apps/blog-sveltekit/config/session.ts b/apps/blog-sveltekit/config/session.ts index 176dd27..57125de 100644 --- a/apps/blog-sveltekit/config/session.ts +++ b/apps/blog-sveltekit/config/session.ts @@ -1,5 +1,15 @@ import { defineSessionConfig, env } from '@holo-js/config' +const configuredSessionSameSite = env('SESSION_SAME_SITE') +const sessionSameSite = configuredSessionSameSite === 'strict' + || configuredSessionSameSite === 'lax' + || configuredSessionSameSite === 'none' + ? configuredSessionSameSite + : 'lax' +const sessionSecure = sessionSameSite === 'none' + ? true + : env('SESSION_SECURE', false) + export default defineSessionConfig({ driver: env('SESSION_DRIVER', 'file'), stores: { @@ -17,9 +27,9 @@ export default defineSessionConfig({ name: env('SESSION_COOKIE', 'holo_session'), path: env('SESSION_PATH', '/'), domain: env('SESSION_DOMAIN'), - secure: env('SESSION_SECURE', false), + secure: sessionSecure, httpOnly: true, - sameSite: env<'lax' | 'strict' | 'none'>('SESSION_SAME_SITE', 'lax'), + sameSite: sessionSameSite, }, idleTimeout: env('SESSION_IDLE_TIMEOUT', 120), absoluteLifetime: env('SESSION_LIFETIME', 120), diff --git a/apps/blog-sveltekit/package.json b/apps/blog-sveltekit/package.json index 6f42a3d..09e28aa 100644 --- a/apps/blog-sveltekit/package.json +++ b/apps/blog-sveltekit/package.json @@ -44,12 +44,12 @@ "@holo-js/session": "^0.1.4", "@holo-js/storage": "^0.1.4", "@holo-js/validation": "workspace:*", - "@sveltejs/adapter-node": "^5.0.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@sveltejs/adapter-node": "catalog:", + "@sveltejs/kit": "catalog:", + "@sveltejs/vite-plugin-svelte": "catalog:", "esbuild": "^0.25.0", - "svelte": "^5.0.0", - "vite": "^5.0.0" + "svelte": "catalog:", + "vite": "catalog:" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/apps/blog-sveltekit/src/lib/schemas/auth.ts b/apps/blog-sveltekit/src/lib/schemas/auth.ts new file mode 100644 index 0000000..0f7a314 --- /dev/null +++ b/apps/blog-sveltekit/src/lib/schemas/auth.ts @@ -0,0 +1,28 @@ +import { field, schema } from '@holo-js/forms/schema' + +export const loginForm = schema({ + email: field.string().required('Email is required.').email('Enter a valid email address.'), + password: field.string().required('Password is required.').min(8, 'Password must be at least 8 characters.'), + remember: field.boolean().default(false), +}) + +export const registerForm = schema({ + name: field.string().required('Name is required.').min(3, 'Name must be at least 3 characters.'), + email: field.string().required('Email is required.').email('Enter a valid email address.'), + password: field.string().required('Password is required.').min(8, 'Password must be at least 8 characters.').confirmed(), + passwordConfirmation: field.string().required('Please confirm your password.'), +}) + +export const forgotPasswordForm = schema({ + email: field.string().required('Email is required.').email('Enter a valid email address.'), +}) + +export const resetPasswordForm = schema({ + token: field.string().required('Reset token is required.'), + password: field.string().required('Password is required.').min(8, 'Password must be at least 8 characters.').confirmed(), + passwordConfirmation: field.string().required('Please confirm your password.'), +}) + +export const verifyEmailForm = schema({ + token: field.string().required('Verification token is required.'), +}) diff --git a/apps/blog-sveltekit/src/routes/+layout.svelte b/apps/blog-sveltekit/src/routes/+layout.svelte index 396de7f..5658268 100644 --- a/apps/blog-sveltekit/src/routes/+layout.svelte +++ b/apps/blog-sveltekit/src/routes/+layout.svelte @@ -4,6 +4,8 @@ blog-sveltekit Posts Admin + Login + Register
diff --git a/apps/blog-sveltekit/src/routes/api/auth/user/+server.ts b/apps/blog-sveltekit/src/routes/api/auth/user/+server.ts new file mode 100644 index 0000000..ccf3056 --- /dev/null +++ b/apps/blog-sveltekit/src/routes/api/auth/user/+server.ts @@ -0,0 +1,10 @@ +import { json } from '@sveltejs/kit' +import { check, user } from '@holo-js/auth' + +export async function GET() { + return json({ + authenticated: await check(), + guard: 'web', + user: await user(), + }) +} diff --git a/apps/blog-sveltekit/src/routes/api/forgot-password/+server.ts b/apps/blog-sveltekit/src/routes/api/forgot-password/+server.ts new file mode 100644 index 0000000..59c1f06 --- /dev/null +++ b/apps/blog-sveltekit/src/routes/api/forgot-password/+server.ts @@ -0,0 +1,32 @@ +import { json } from '@sveltejs/kit' +import { requestPasswordReset } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { forgotPasswordForm } from '$lib/schemas/auth' + +export async function POST({ request }: { request: Request }) { + const submission = await validate(request, forgotPasswordForm) + + if (!submission.valid) { + return json(submission.fail(), { + status: submission.fail().status, + }) + } + + const { error } = await requestPasswordReset(submission.data) + if (error) { + return json({ + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + }, { + status: error.status, + }) + } + + return json(submission.success({ + message: 'If an account exists for that email, a reset link has been sent.', + })) +} diff --git a/apps/blog-sveltekit/src/routes/api/login/+server.ts b/apps/blog-sveltekit/src/routes/api/login/+server.ts new file mode 100644 index 0000000..c25a103 --- /dev/null +++ b/apps/blog-sveltekit/src/routes/api/login/+server.ts @@ -0,0 +1,47 @@ +import { json } from '@sveltejs/kit' +import { login } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { loginForm } from '$lib/schemas/auth' + +export async function POST({ request }: { request: Request }) { + const submission = await validate(request, loginForm, { + throttle: 'login', + }) + + if (!submission.valid) { + return json(submission.fail(), { + status: submission.fail().status, + }) + } + + const { data: session, error } = await login(submission.data) + if (error) { + return json({ + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + }, { + status: error.status, + }) + } + + const headers = new Headers() + for (const cookie of session.cookies) { + headers.append('set-cookie', cookie) + } + + return json(submission.success({ + message: session.emailVerificationRequired + ? 'Signed in. Verify your email address to continue.' + : 'Signed in successfully.', + redirectTo: session.emailVerificationRequired + ? session.emailVerificationRoute ?? '/verify-email' + : '/admin', + user: session.user, + }), { + headers, + }) +} diff --git a/apps/blog-sveltekit/src/routes/api/logout/+server.ts b/apps/blog-sveltekit/src/routes/api/logout/+server.ts new file mode 100644 index 0000000..b628d5c --- /dev/null +++ b/apps/blog-sveltekit/src/routes/api/logout/+server.ts @@ -0,0 +1,19 @@ +import { json } from '@sveltejs/kit' +import { logout, user } from '@holo-js/auth' + +export async function POST() { + const signedOut = await logout() + const headers = new Headers() + for (const cookie of signedOut.cookies) { + headers.append('set-cookie', cookie) + } + + return json({ + ok: true, + authenticated: false, + message: 'Signed out successfully.', + user: await user(), + }, { + headers, + }) +} diff --git a/apps/blog-sveltekit/src/routes/api/register/+server.ts b/apps/blog-sveltekit/src/routes/api/register/+server.ts new file mode 100644 index 0000000..05b6e58 --- /dev/null +++ b/apps/blog-sveltekit/src/routes/api/register/+server.ts @@ -0,0 +1,49 @@ +import { json } from '@sveltejs/kit' +import { loginUsing, register } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { registerForm } from '$lib/schemas/auth' + +export async function POST({ request }: { request: Request }) { + const submission = await validate(request, registerForm, { + throttle: 'register', + }) + + if (!submission.valid) { + return json(submission.fail(), { + status: submission.fail().status, + }) + } + + const { data: created, error } = await register(submission.data) + if (error) { + return json({ + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + }, { + status: error.status, + }) + } + + const session = await loginUsing(created) + const headers = new Headers() + for (const cookie of session.cookies) { + headers.append('set-cookie', cookie) + } + + return json(submission.success({ + message: session.emailVerificationRequired + ? 'Account created. Check your inbox to verify your email address.' + : 'Account created and signed in successfully.', + redirectTo: session.emailVerificationRequired + ? session.emailVerificationRoute ?? '/verify-email' + : '/admin', + user: session.user, + }, 201), { + status: 201, + headers, + }) +} diff --git a/apps/blog-sveltekit/src/routes/api/reset-password/+server.ts b/apps/blog-sveltekit/src/routes/api/reset-password/+server.ts new file mode 100644 index 0000000..288fb9b --- /dev/null +++ b/apps/blog-sveltekit/src/routes/api/reset-password/+server.ts @@ -0,0 +1,33 @@ +import { json } from '@sveltejs/kit' +import { resetPassword } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { resetPasswordForm } from '$lib/schemas/auth' + +export async function POST({ request }: { request: Request }) { + const submission = await validate(request, resetPasswordForm) + + if (!submission.valid) { + return json(submission.fail(), { + status: submission.fail().status, + }) + } + + const { error } = await resetPassword(submission.data) + if (error) { + return json({ + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + }, { + status: error.status, + }) + } + + return json(submission.success({ + message: 'Password reset successfully. You can sign in with your new password.', + redirectTo: '/login', + })) +} diff --git a/apps/blog-sveltekit/src/routes/api/verify-email/+server.ts b/apps/blog-sveltekit/src/routes/api/verify-email/+server.ts new file mode 100644 index 0000000..989d633 --- /dev/null +++ b/apps/blog-sveltekit/src/routes/api/verify-email/+server.ts @@ -0,0 +1,33 @@ +import { json } from '@sveltejs/kit' +import { verification } from '@holo-js/auth' +import { sanitizeFlashedInput, validate } from '@holo-js/forms' + +import { verifyEmailForm } from '$lib/schemas/auth' + +export async function POST({ request }: { request: Request }) { + const submission = await validate(request, verifyEmailForm) + + if (!submission.valid) { + return json(submission.fail(), { + status: submission.fail().status, + }) + } + + const { error } = await verification.consume(submission.data.token) + if (error) { + return json({ + ok: false as const, + status: error.status, + valid: false as const, + values: sanitizeFlashedInput(submission.values), + errors: error.fields, + }, { + status: error.status, + }) + } + + return json(submission.success({ + message: 'Email address verified. You can sign in now.', + redirectTo: '/login', + })) +} diff --git a/apps/blog-sveltekit/src/routes/api/verify-email/resend/+server.ts b/apps/blog-sveltekit/src/routes/api/verify-email/resend/+server.ts new file mode 100644 index 0000000..edd2f76 --- /dev/null +++ b/apps/blog-sveltekit/src/routes/api/verify-email/resend/+server.ts @@ -0,0 +1,43 @@ +import { json } from '@sveltejs/kit' +import { verification } from '@holo-js/auth' + +interface ResendVerificationRequestBody { + readonly email?: string +} + +async function readRequestBody(request: Request): Promise { + const contentType = request.headers.get('content-type') ?? '' + if (!contentType.includes('application/json')) { + return {} + } + + const payload = await request.json().catch(() => null) + if (!payload || typeof payload !== 'object') { + return {} + } + + const email = typeof payload.email === 'string' ? payload.email.trim() : undefined + return email ? { email } : {} +} + +export async function POST({ request }: { request: Request }) { + const input = await readRequestBody(request) + const { error } = await verification.resend(input) + if (error) { + return json({ + ok: false as const, + status: error.status, + errors: error.fields, + }, { + status: error.status, + }) + } + + return json({ + ok: true as const, + status: 200, + data: { + message: 'A fresh verification email has been sent.', + }, + }) +} diff --git a/apps/blog-sveltekit/src/routes/forgot-password/+page.svelte b/apps/blog-sveltekit/src/routes/forgot-password/+page.svelte new file mode 100644 index 0000000..4192457 --- /dev/null +++ b/apps/blog-sveltekit/src/routes/forgot-password/+page.svelte @@ -0,0 +1,50 @@ + + +
+
+

Forgot password

+

Request a password reset link for your local account.

+
+ +
{ event.preventDefault(); form.submit() }}> + + + +
+ + {#if form.lastSubmission?.ok === true} +

A password reset link has been sent if the account exists.

+ {/if} + + Back to sign in +
+ + diff --git a/apps/blog-sveltekit/src/routes/login/+page.svelte b/apps/blog-sveltekit/src/routes/login/+page.svelte new file mode 100644 index 0000000..84472c5 --- /dev/null +++ b/apps/blog-sveltekit/src/routes/login/+page.svelte @@ -0,0 +1,75 @@ + + +
+
+

Sign in

+

Use your email address and password to access the admin area.

+
+ +
{ event.preventDefault(); form.submit() }}> + + + + + + + +
+ + {#if form.lastSubmission?.ok === true} +
+

Signed in successfully.

+ Continue to admin +
+ {/if} + + +
+ + diff --git a/apps/blog-sveltekit/src/routes/register/+page.svelte b/apps/blog-sveltekit/src/routes/register/+page.svelte new file mode 100644 index 0000000..ccbacd1 --- /dev/null +++ b/apps/blog-sveltekit/src/routes/register/+page.svelte @@ -0,0 +1,87 @@ + + +
+
+

Create account

+

Create a local user account and verify the email address before signing in.

+
+ +
{ event.preventDefault(); form.submit() }}> + + + + + + + + + +
+ + {#if form.lastSubmission?.ok === true} +
+

Account created. Check your inbox to verify your email address.

+ Return to sign in +
+ {/if} + + Already have an account? +
+ + diff --git a/apps/blog-sveltekit/src/routes/reset-password/+page.server.ts b/apps/blog-sveltekit/src/routes/reset-password/+page.server.ts new file mode 100644 index 0000000..f992582 --- /dev/null +++ b/apps/blog-sveltekit/src/routes/reset-password/+page.server.ts @@ -0,0 +1,5 @@ +export function load({ url }: { url: URL }) { + return { + token: url.searchParams.get('token') ?? '', + } +} diff --git a/apps/blog-sveltekit/src/routes/reset-password/+page.svelte b/apps/blog-sveltekit/src/routes/reset-password/+page.svelte new file mode 100644 index 0000000..ec21502 --- /dev/null +++ b/apps/blog-sveltekit/src/routes/reset-password/+page.svelte @@ -0,0 +1,78 @@ + + +
+
+

Reset password

+

Set a new password using the reset link from your email.

+
+ + {#if data.token} +
{ event.preventDefault(); form.submit() }}> + + + + + + + {#if form.errors.has('token')} + {form.errors.first('token')} + {/if} + + +
+ {:else} +

A reset token is required to complete this form.

+ {/if} + + {#if form.lastSubmission?.ok === true} +
+

Your password has been reset successfully.

+ Sign in +
+ {/if} +
+ + diff --git a/apps/blog-sveltekit/src/routes/verify-email/+page.server.ts b/apps/blog-sveltekit/src/routes/verify-email/+page.server.ts new file mode 100644 index 0000000..ccbb945 --- /dev/null +++ b/apps/blog-sveltekit/src/routes/verify-email/+page.server.ts @@ -0,0 +1,6 @@ +export function load({ url }: { url: URL }) { + return { + email: url.searchParams.get('email') ?? '', + token: url.searchParams.get('token') ?? '', + } +} diff --git a/apps/blog-sveltekit/src/routes/verify-email/+page.svelte b/apps/blog-sveltekit/src/routes/verify-email/+page.svelte new file mode 100644 index 0000000..79b573e --- /dev/null +++ b/apps/blog-sveltekit/src/routes/verify-email/+page.svelte @@ -0,0 +1,107 @@ + + +
+
+

Verify your email

+

Use the verification link from your inbox to confirm the account.

+
+ + {#if data.token} +
{ event.preventDefault(); form.submit() }}> + + {#if form.errors.has('token')} + {form.errors.first('token')} + {/if} + +
+ {:else} +
+

+ {data.email + ? `Check ${data.email} for the verification email, then open the link from this page.` + : 'Check your inbox for the verification email, then open the link from this page.'} +

+ + {#if resendMessage} +

{resendMessage}

+ {/if} + {#if resendError} +

{resendError}

+ {/if} +
+ {/if} + + {#if form.lastSubmission?.ok === true} +
+

Your email address has been verified.

+ Sign in +
+ {/if} + + +
+ + diff --git a/apps/blog-sveltekit/tests/run.mjs b/apps/blog-sveltekit/tests/run.mjs index 7f28048..b8901f1 100644 --- a/apps/blog-sveltekit/tests/run.mjs +++ b/apps/blog-sveltekit/tests/run.mjs @@ -3,17 +3,42 @@ import { spawn } from 'node:child_process' import { existsSync } from 'node:fs' import { readFile, rm, writeFile } from 'node:fs/promises' import { get } from 'node:http' +import { createServer } from 'node:net' import { join } from 'node:path' import { pathToFileURL } from 'node:url' +import { assertExampleAppAuthFlow } from '../../../tests/example-app-auth-flow.mjs' const cwd = process.cwd() const configPath = join(cwd, 'config/app.ts') const originalConfig = await readFile(configPath, 'utf8') const runtimeSchemaPath = join(cwd, '.holo-js/generated/schema.mjs') +const port = await new Promise((resolve, reject) => { + const server = createServer() + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + const address = server.address() + if (!address || typeof address === 'string') { + reject(new Error('Could not determine an available port.')) + return + } + + const selected = String(address.port) + server.close((error) => { + if (error) { + reject(error) + return + } + + resolve(selected) + }) + }) +}) +let capturedOutput = '' function createChildEnv(overrides = {}) { const env = { ...process.env, + HOLO_SECURITY_TRUST_PROXY: 'true', ...overrides, } @@ -134,7 +159,9 @@ function pipeOutput(stream, target, onLine) { let buffered = '' stream.on('data', chunk => { - buffered += chunk.toString() + const text = chunk.toString() + capturedOutput += text + buffered += text const lines = buffered.split(/\r?\n/) buffered = lines.pop() ?? '' for (const line of lines) { @@ -219,10 +246,14 @@ try { await run('bun', ['x', 'holo', 'migrate:fresh', '--seed']) await run('npx', ['tsx', 'tests/blog-logic.mjs']) - child = spawn('bun', ['run', 'dev'], { + child = spawn('bun', ['x', 'vite', 'dev', '--host', 'localhost', '--port', port], { cwd, detached: true, - env: createChildEnv(), + env: createChildEnv({ + APP_URL: `http://localhost:${port}`, + MAIL_MAILER: 'log', + MAIL_LOG_BODIES: 'true', + }), stdio: ['inherit', 'pipe', 'pipe'], }) @@ -232,6 +263,11 @@ try { assert.equal(initial.app, 'blog-sveltekit') await waitForText(`${devUrl}/`, payload => payload.includes('Shipping a Real Holo Blog on SvelteKit')) await waitForText(`${devUrl}/admin/posts`, payload => payload.includes('Designing the Example App Roadmap')) + await assertExampleAppAuthFlow({ + baseUrl: devUrl, + getOutput: () => capturedOutput, + appName: 'blog-sveltekit', + }) await writeFile(configPath, originalConfig.replace("name: env('APP_NAME', 'blog-sveltekit')", "name: env('APP_NAME', 'blog-sveltekit-updated')")) await new Promise(resolve => setTimeout(resolve, 3000)) diff --git a/apps/blog-sveltekit/vite.config.ts b/apps/blog-sveltekit/vite.config.ts index efb2cb3..482d3ac 100644 --- a/apps/blog-sveltekit/vite.config.ts +++ b/apps/blog-sveltekit/vite.config.ts @@ -11,12 +11,41 @@ export default defineConfig({ ssr: { external: [ '@holo-js/adapter-sveltekit', + '@holo-js/auth', + '@holo-js/auth-clerk', + '@holo-js/auth-social', + '@holo-js/auth-workos', + '@holo-js/authorization', + '@holo-js/broadcast', + '@holo-js/cache', + '@holo-js/cache-db', + '@holo-js/cache-redis', '@holo-js/config', '@holo-js/core', '@holo-js/db', + '@holo-js/db-mysql', + '@holo-js/db-postgres', + '@holo-js/db-sqlite', + '@holo-js/events', + '@holo-js/flux', + '@holo-js/flux-svelte', + '@holo-js/forms', + '@holo-js/mail', + '@holo-js/media', + '@holo-js/notifications', + '@holo-js/queue', + '@holo-js/queue-db', + '@holo-js/queue-redis', + '@holo-js/security', + '@holo-js/session', '@holo-js/storage', '@holo-js/storage/runtime', + '@holo-js/storage-s3', + '@holo-js/validation', 'better-sqlite3', + 'ioredis', + 'mysql2', + 'pg', ], }, }) diff --git a/apps/docs/docs/auth/current-auth-client.md b/apps/docs/docs/auth/current-auth-client.md index 22fa9c5..26113c7 100644 --- a/apps/docs/docs/auth/current-auth-client.md +++ b/apps/docs/docs/auth/current-auth-client.md @@ -7,6 +7,7 @@ The client package is intentionally small. It does not implement authentication. It calls a current-auth endpoint owned by your application and returns: +- `useAuth()` - `user()` - `refreshUser()` - `check()` @@ -43,13 +44,23 @@ Optional configuration: ## Usage ```ts -import { check, refreshUser, user } from '@holo-js/auth/client' +import { check, refreshUser, useAuth, user } from '@holo-js/auth/client' -const current = await user() -const authenticated = await check() -const fresh = await refreshUser() +const auth = await useAuth() +const current = auth.user +const authenticated = auth.check() +const fresh = await auth.refreshUser() + +// direct helpers still work too +await user() +await check() +await refreshUser() ``` +`useAuth()` returns the full current-auth payload from your endpoint, so client code can read `auth.user`, +`auth.authenticated`, and `auth.guard` from one object, while still exposing `auth.check()` and +`auth.refreshUser()`. + `user()` may return cached state. `refreshUser()` forces a new request to the endpoint. ## Implementing The Endpoint diff --git a/apps/docs/docs/auth/email-verification.md b/apps/docs/docs/auth/email-verification.md index a7da983..d7a24e2 100644 --- a/apps/docs/docs/auth/email-verification.md +++ b/apps/docs/docs/auth/email-verification.md @@ -1,295 +1,165 @@ # Email Verification -Email verification lets the application require verified email addresses before a user can continue. +Email verification lets the application require a verified address while keeping the delivery flow automatic. ## Introduction -When email verification is enabled, login is blocked until the local user model has a verification timestamp. +When email verification is enabled: -```ts -emailVerification: { - required: true, -} -``` - -The local model should have an `email_verified_at` column. +- registration automatically creates and sends a verification email +- login is still allowed +- the returned session tells the route that verification is still required +- the framework-generated email link uses `APP_URL` plus the configured verification route -## Creating Verification Tokens +Enable it in `config/auth.ts`: ```ts -import { verification } from '@holo-js/auth' +import { defineAuthConfig, env } from '@holo-js/config' -const token = await verification.create(user) +export default defineAuthConfig({ + emailVerification: { + required: true, + route: env('AUTH_EMAIL_VERIFICATION_ROUTE', '/verify-email'), + }, +}) ``` -The token store records: +The local model should have an `email_verified_at` column. -- provider -- user id -- email -- hashed token secret -- creation time -- expiration time +## Environment -## Consuming Verification Tokens +Set the application URL and optional route override: -```ts -await verification.consume(token.plainTextToken) +```dotenv +APP_URL=http://localhost:3000 +AUTH_EMAIL_VERIFICATION_ROUTE=/verify-email ``` -The verification flow marks the local user as verified and invalidates the token. +`APP_URL` is used when the framework builds the email link. Applications should not manually construct the +verification URL in normal usage. + +The application still owns the verification page and the route that calls `verification.consume(...)`. The framework +owns the redirect target and the generated email link. ## Registration Flow -After registration succeeds, the application should request a verification token and deliver it. This -example sends through `@holo-js/notifications` directly; auth-managed delivery only works after the -auth runtime has a delivery hook configured. See [Runtime Delivery Hook Configuration](#runtime-delivery-hook-configuration). +Registration automatically starts email verification when `emailVerification.required` is `true`: ```ts -import { register, verification } from '@holo-js/auth' -import { defineNotification, notify } from '@holo-js/notifications' - -const verificationCreated = (token: { - id: string - plainTextToken: string - expiresAt: Date -}) => defineNotification({ - type: 'auth.email-verification', - via() { - return ['email'] as const - }, - build: { - email(user: { name?: string }) { - return { - subject: 'Verify your email address', - greeting: `Hello ${user.name ?? 'there'},`, - lines: ['Please verify your email address to continue.'], - action: { - label: 'Verify email', - url: `https://app.test/verify-email?token=${encodeURIComponent(token.plainTextToken)}`, - }, - } - } - }, -}) +import { register } from '@holo-js/auth' -const created = await register({ +const { data: created, error } = await register({ + name: body.name, email: body.email, password: body.password, passwordConfirmation: body.passwordConfirmation, }) - -const token = await verification.create(created) - -// This sends through notifications directly. -// Auth-managed delivery requires a configured AuthDeliveryHook. -await notify(created, verificationCreated(token)) ``` -## Delivery Integration +Expected registration failures come back in `error`. On success, the local user is created and the verification +message is delivered automatically through the configured auth delivery integration. -Package installation alone does not enable auth email delivery. The auth runtime must have an -`AuthDeliveryHook` bound before auth-managed verification emails can be sent. +Applications do not need to call `verification.create(...)` after registration just to send the first email. -See [Runtime Delivery Hook Configuration](#runtime-delivery-hook-configuration) for the runtime binding -you need when routing auth delivery through notifications or mail. +## Login Flow -### Flexible Delivery Options -Once you call notify, you can send the verification through any available channel: -- **Email**: Primary delivery method for verification links (uses mail system when available) -- **Database**: Store verification records for internal tracking -- **Broadcast**: Real-time verification status updates via websocket -- **Custom Channels**: Extend verification delivery to Slack, SMS, or other services - -### Queue-Friendly Delivery -Verification notifications can be queued for background processing: +Unverified users can still sign in. The returned session includes verification state: ```ts -await notify(created, verificationCreated(token)) - .onQueue('auth') -``` - -### Transaction Safety -Verification delivery respects database transactions: +import { login } from '@holo-js/auth' -```ts -await notify(created, verificationCreated(token)) - .afterCommit() // Send only after DB transaction commits -``` - -## Customizing Verification Emails - -You can fully customize the verification notification by modifying the `build.email()` function: - -```ts -const verificationCreated = (token: { - id: string - plainTextToken: string - expiresAt: Date -}) => defineNotification({ - type: 'auth.email-verification', - via() { - return ['email', 'database'] as const // Send to both email and database - }, - build: { - email(user: { name?: string }) { - return { - subject: 'Verify your Holo JS account', - greeting: `Hello ${user.name ?? 'there'},`, - lines: [ - 'Thanks for signing up! Please verify your email address to get started.', - 'This verification link will expire in 24 hours.' - ], - action: { - label: 'Verify Email Address', - url: `https://app.test/verify-email?token=${encodeURIComponent(token.plainTextToken)}`, - }, - // Add any additional email-specific properties here - } - }, - database() { - return { - verificationTokenId: token.id, - expiresAt: token.expiresAt, - purpose: 'email-verification' - } - } - } +const { data: session, error } = await login({ + email: body.email, + password: body.password, + remember: body.remember === true, }) ``` -## Delivery Mechanism - -When the `notify()` function is called for email verification: - -1. **If `@holo-js/notifications` is installed**: The notification is sent through the notifications system -2. **Notifications → Mail**: If the notifications email channel is configured to use mail, the email is sent through the mailer -3. **Auth Runtime Delivery Hook**: Built-in auth delivery requires a configured `AuthDeliveryHook`; otherwise auth logs a warning and skips delivery -4. **No Delivery Binding**: If no delivery hook is bound, verification tokens are created but are not sent automatically - -## Installation - -To enable email verification delivery through notifications or mail: - -```bash -# For full notifications + mail integration (recommended) -npx holo install notifications -npx holo install mail +When verification is still required, successful login includes: -# For mail-only delivery -npx holo install mail -``` - -## Protecting Application Routes +- `emailVerificationRequired: true` +- `emailVerificationRoute: '/verify-email?email=ava%40example.com'` -Route protection remains in your application. A simple pattern is: +Typical route handling: ```ts -import { user } from '@holo-js/auth' - -const current = await user() -if (!current?.email_verified_at) { - return Response.json({ message: 'Email verification required.' }, { status: 403 }) +if (error) { + return Response.json({ + ok: false, + status: error.status, + valid: false, + values: body, + errors: error.fields, + }, { status: error.status }) } -``` -The local model should have an `email_verified_at` column. - -## Creating Verification Tokens - -```ts -import { verification } from '@holo-js/auth' - -const token = await verification.create(user) +return Response.json({ + ok: true, + data: { + message: session.emailVerificationRequired + ? 'Signed in. Verify your email address to continue.' + : 'Signed in successfully.', + redirectTo: session.emailVerificationRequired + ? session.emailVerificationRoute ?? '/verify-email' + : '/admin', + }, +}) ``` -The token store records: - -- provider -- user id -- email -- hashed token secret -- creation time -- expiration time +That lets the app redirect the signed-in user to the verify page instead of rejecting the login attempt. ## Consuming Verification Tokens +Verification pages consume the token from the emailed link: + ```ts -await verification.consume(token.plainTextToken) +import { verification } from '@holo-js/auth' + +const { data: verifiedUser, error } = await verification.consume(token) ``` The verification flow marks the local user as verified and invalidates the token. -## Registration Flow +## Resending Verification Emails -After registration succeeds, the application should request a verification token and deliver it. This -example again uses notifications directly; use [Runtime Delivery Hook Configuration](#runtime-delivery-hook-configuration) -if you want auth-managed delivery through notifications or mail. +Applications can resend another verification email with plain object input: ```ts -import { register, verification } from '@holo-js/auth' -import { defineNotification, notify } from '@holo-js/notifications' - -const verificationCreated = (token: { - id: string - plainTextToken: string - expiresAt: Date -}) => defineNotification({ - type: 'auth.email-verification', - via() { - return ['email'] as const - }, - build: { - email(user: { name?: string }) { - return { - subject: 'Verify your email address', - greeting: `Hello ${user.name ?? 'there'},`, - lines: ['Please verify your email address to continue.'], - action: { - label: 'Verify email', - url: `https://app.test/verify-email?token=${encodeURIComponent(token.plainTextToken)}`, - }, - } - }, - }, -}) +import { verification } from '@holo-js/auth' -const created = await register({ +const { error } = await verification.resend({ email: body.email, - password: body.password, - passwordConfirmation: body.passwordConfirmation, }) +``` -const token = await verification.create(created) +This is the intended verify-page flow when the user lands on `/verify-email?email=...` after login. -await notify(created, verificationCreated(token)) -``` +Expected resend failures come back in `error`, for example: -## Delivery +- `email_verification_user_missing` +- `email_already_verified` -Auth delivery requires runtime configuration. Bind an `AuthDeliveryHook` through auth runtime bindings, -or follow [Runtime Delivery Hook Configuration](#runtime-delivery-hook-configuration), before relying on -notifications or mail for auth-managed verification emails. +## Delivery -Install notifications or mail into an existing project with: +Verification delivery is automatic once auth delivery is available. -```bash -npx holo install notifications -npx holo install mail -``` +- if `@holo-js/notifications` is installed, core can route auth delivery through notifications +- if notifications are absent but `@holo-js/mail` is installed, core can send directly through mail +- if no delivery integration is installed, auth logs the skipped delivery instead of building links in app code -## Runtime Delivery Hook Configuration +The generated verification email includes: -Auth delivery is disabled until the auth runtime is given a delivery hook. Bind `delivery` with -`configureAuthRuntime({ delivery: ... })`, or follow the runtime bootstrap that wires auth, notifications, -and mail together before relying on the built-in auth delivery flows. +- an HTML body +- a text fallback +- a link built from `APP_URL` +- the configured verification route +- the signed verification token -The manual `notify(created, verificationCreated(token))` example above does not use `AuthDeliveryHook`; -it sends through notifications directly. +Applications do not need to manually compose `https://app.test/verify-email?token=...` links in normal usage. ## Protecting Application Routes -Route protection remains in your application. A simple pattern is: +Route protection still belongs to the application. Email verification does not automatically block arbitrary pages. ```ts import { user } from '@holo-js/auth' @@ -299,3 +169,8 @@ if (!current?.email_verified_at) { return Response.json({ message: 'Email verification required.' }, { status: 403 }) } ``` + +## Related Guides + +- [Local Authentication](/auth/local-auth) +- [Password Reset](/auth/password-reset) diff --git a/apps/docs/docs/auth/index.md b/apps/docs/docs/auth/index.md index 9bdd00e..a13dd07 100644 --- a/apps/docs/docs/auth/index.md +++ b/apps/docs/docs/auth/index.md @@ -67,7 +67,7 @@ Start with the auth and session config files: ```ts // config/auth.ts -import { defineAuthConfig } from '@holo-js/config' +import { defineAuthConfig, env } from '@holo-js/config' export default defineAuthConfig({ defaults: { @@ -90,6 +90,19 @@ export default defineAuthConfig({ identifiers: ['email'], }, }, + emailVerification: { + required: true, + route: env('AUTH_EMAIL_VERIFICATION_ROUTE', '/verify-email'), + }, + passwords: { + users: { + provider: 'users', + table: 'password_reset_tokens', + expire: 60, + throttle: 60, + route: env('AUTH_PASSWORD_RESET_ROUTE', '/reset-password'), + }, + }, }) ``` @@ -114,30 +127,67 @@ Then use auth operations inside your own routes: ```ts import { login, logout, refreshUser, register, user } from '@holo-js/auth' +function sanitizeAuthBody(body: Record) { + const sanitizedBody = { ...body } + delete sanitizedBody.password + delete sanitizedBody.passwordConfirmation + delete sanitizedBody.confirmPassword + delete sanitizedBody.currentPassword + delete sanitizedBody.newPassword + delete sanitizedBody.token + + return sanitizedBody +} + export async function POST(request: Request) { const body = await request.json() + const sanitizedBody = sanitizeAuthBody(body) - const created = await register({ + const { data: created, error: registerError } = await register({ name: body.name, email: body.email, password: body.password, passwordConfirmation: body.passwordConfirmation, }) + if (registerError) { + return Response.json({ + ok: false, + status: registerError.status, + valid: false, + values: sanitizedBody, + errors: registerError.fields, + }, { status: registerError.status }) + } + return Response.json(created, { status: 201 }) } export async function PUT(request: Request) { const body = await request.json() + const sanitizedBody = sanitizeAuthBody(body) - await login({ + const { data: session, error } = await login({ email: body.email, password: body.password, remember: body.remember === true, }) + if (error) { + return Response.json({ + ok: false, + status: error.status, + valid: false, + values: sanitizedBody, + errors: error.fields, + }, { status: error.status }) + } + return Response.json({ authenticated: true, + redirectTo: session.emailVerificationRequired + ? session.emailVerificationRoute ?? '/verify-email' + : '/admin', user: await refreshUser(), }) } @@ -152,6 +202,9 @@ export async function DELETE() { } ``` +When email verification is enabled, successful login can redirect the user to the configured verification route +instead of rejecting the login attempt. + ## Retrieving The Authenticated User Use the default export or direct named exports: @@ -208,16 +261,48 @@ payload to `login()` or `register()`. ```ts import { login } from '@holo-js/auth' -await login({ +const { data, error } = await login({ email: 'ava@example.com', password: 'secret-secret', }) ``` +Successful auth calls put the result in `data`. Expected auth failures come back in `error`. + +`error` is plain data: + +```ts +{ + code: 'invalid_credentials', + message: 'These credentials do not match our records.', + status: 401, + fields: { + email: ['These credentials do not match our records.'], + password: ['These credentials do not match our records.'], + }, +} +``` + +That means your route can forward auth failures directly without any helper layer: + +```ts +const sanitizedBody = sanitizeAuthBody(body) + +if (error) { + return Response.json({ + ok: false, + status: error.status, + valid: false, + values: sanitizedBody, + errors: error.fields, + }, { status: error.status }) +} +``` + The auth runtime uses the validated payload itself. If your credentials are based on `phone`, pass `phone`. ```ts -await login({ +const { data, error } = await login({ phone: '20123456789', password: 'secret-secret', }) diff --git a/apps/docs/docs/auth/local-auth.md b/apps/docs/docs/auth/local-auth.md index b43cc22..6a9a432 100644 --- a/apps/docs/docs/auth/local-auth.md +++ b/apps/docs/docs/auth/local-auth.md @@ -21,7 +21,7 @@ The package does not require hardcoded request fields such as `email`. The crede Define a session guard and a provider: ```ts -import { defineAuthConfig } from '@holo-js/config' +import { defineAuthConfig, env } from '@holo-js/config' export default defineAuthConfig({ defaults: { @@ -45,11 +45,29 @@ export default defineAuthConfig({ }, }, emailVerification: { - required: false, + required: true, + route: env('AUTH_EMAIL_VERIFICATION_ROUTE', '/verify-email'), + }, + passwords: { + users: { + provider: 'users', + table: 'password_reset_tokens', + expire: 60, + throttle: 60, + route: env('AUTH_PASSWORD_RESET_ROUTE', '/reset-password'), + }, }, }) ``` +Related environment values: + +```dotenv +APP_URL=http://localhost:3000 +AUTH_EMAIL_VERIFICATION_ROUTE=/verify-email +AUTH_PASSWORD_RESET_ROUTE=/reset-password +``` + ## Identifiers Identifiers are the fields auth uses to find and de-duplicate users. @@ -118,7 +136,7 @@ Use `register()` inside your route after validation succeeds: ```ts import { register } from '@holo-js/auth' -const created = await register({ +const { data: created, error } = await register({ name: body.name, email: body.email, phone: body.phone, @@ -129,14 +147,32 @@ const created = await register({ }) ``` +Expected auth failures come back in `error`. Successful calls put the created user in `data`. + +The auth failure object is plain data: + +```ts +{ + code: 'registration_identifier_taken', + message: 'A user with this email already exists.', + status: 422, + fields: { + email: ['A user with this email already exists.'], + }, +} +``` + The local provider creates the model record and hashes the password before it is stored. Extra fields like `country` and `dob` are saved as attributes, but they are not treated as auth identifiers unless you explicitly add them to the provider's `identifiers`. +When `emailVerification.required` is `true`, successful registration also starts the verification flow automatically. +Applications do not need to manually call `verification.create(...)` just to send the first verification email. + If your application uses another identifier, pass that identifier instead: ```ts -await register({ +const { error } = await register({ phone: body.phone, country: body.country, password: body.password, @@ -151,7 +187,7 @@ These APIs are server-side APIs from `@holo-js/auth`. They are not available fro ```ts import { login } from '@holo-js/auth' -await login({ +const { data: session, error } = await login({ email: body.email, password: body.password, remember: body.remember === true, @@ -165,6 +201,13 @@ On successful login, the session guard: - stores the authenticated user payload in session state - optionally issues a remember-me token +When email verification is enabled and the user is still unverified, successful login also includes: + +- `emailVerificationRequired: true` +- `emailVerificationRoute: '/verify-email?email=...'` + +That lets the route redirect the user to the verification page while keeping them signed in. + ## Signing In Trusted Users Use trusted login when your application has already resolved the user and should establish a local session without @@ -227,7 +270,7 @@ This is useful when: ### `hashPassword(password)` -Use this when you need a password digest but you are not going through `register()` or `passwords.consume()`. +Use this when you need a password digest but you are not going through `register()` or `resetPassword()`. Typical cases: @@ -330,7 +373,7 @@ Typical flow: Set `remember: true` during login: ```ts -await login({ +const { error } = await login({ email: body.email, password: body.password, remember: true, @@ -386,12 +429,29 @@ import { login } from '@holo-js/auth' export async function POST(request: Request) { const body = await request.json() - await login({ + const { data: session, error } = await login({ email: body.email, password: body.password, }) - return Response.json({ ok: true }) + if (error) { + return Response.json({ + ok: false, + status: error.status, + valid: false, + values: body, + errors: error.fields, + }, { status: error.status }) + } + + return Response.json({ + ok: true, + data: { + redirectTo: session.emailVerificationRequired + ? session.emailVerificationRoute ?? '/verify-email' + : '/admin', + }, + }) } ``` diff --git a/apps/docs/docs/auth/password-reset.md b/apps/docs/docs/auth/password-reset.md index e6ac271..9441041 100644 --- a/apps/docs/docs/auth/password-reset.md +++ b/apps/docs/docs/auth/password-reset.md @@ -7,22 +7,54 @@ Password reset tokens let the application issue one-time credentials for resetti Password reset uses the configured broker and local provider: ```ts -passwords: { - users: { - provider: 'users', - table: 'password_reset_tokens', - expire: 60, - throttle: 60, +import { defineAuthConfig, env } from '@holo-js/config' + +export default defineAuthConfig({ + passwords: { + users: { + provider: 'users', + table: 'password_reset_tokens', + expire: 60, + throttle: 60, + route: env('AUTH_PASSWORD_RESET_ROUTE', '/reset-password'), + }, }, -} +}) ``` +Set the route and base app URL in the environment: + +```dotenv +APP_URL=http://localhost:3000 +AUTH_PASSWORD_RESET_ROUTE=/reset-password +``` + +The framework-generated reset email uses `APP_URL` plus the configured broker route automatically. + +The application still owns the reset page and the route that calls `resetPassword(...)`. The framework owns the +generated link target and delivery URL composition. + ## Requesting A Reset Token ```ts -import { passwords } from '@holo-js/auth' +import { requestPasswordReset } from '@holo-js/auth' -await passwords.request('ava@example.com') +const { error } = await requestPasswordReset({ + email: 'ava@example.com', +}) +``` + +If the request fails for an expected auth reason, `error` is plain data: + +```ts +{ + code: 'password_reset_email_required', + message: 'Email is required to request a password reset.', + status: 422, + fields: { + email: ['Email is required to request a password reset.'], + }, +} ``` The flow: @@ -30,18 +62,23 @@ The flow: - looks up the user through the configured broker provider - invalidates older tokens for that email - creates a new hashed reset token -- sends the token through the configured delivery hook +- sends the reset email through the configured delivery hook ## Resetting The Password ```ts -await passwords.consume({ +import { resetPassword } from '@holo-js/auth' + +const { data: resetUser, error } = await resetPassword({ token: body.token, password: body.password, passwordConfirmation: body.passwordConfirmation, }) ``` +`error.fields` targets the submitted auth fields directly, such as `token`, `password`, and +`passwordConfirmation`. + The reset flow verifies the token, hashes the new password, updates the local user record, and invalidates the used token. @@ -50,45 +87,23 @@ token. Use a non-default broker when needed: ```ts -await passwords.request('admin@example.com', { +const { error } = await requestPasswordReset({ + email: 'admin@example.com', +}, { broker: 'admins', }) ``` ## Delivery -Password reset delivery works the same way as email verification: auth creates the token, and notifications or -direct mail can own delivery. +Password reset delivery works the same way as email verification: -For explicit delivery, send on-demand email routes through `notifyUsing()`: - -```ts -import { defineNotification, notifyUsing } from '@holo-js/notifications' - -const passwordResetRequested = (token: { plainTextToken: string }) => defineNotification({ - type: 'auth.password-reset', - via() { - return ['email'] as const - }, - build: { - email() { - return { - subject: 'Reset your password', - lines: ['Use the link below to reset your password.'], - action: { - label: 'Reset password', - url: `https://app.test/reset-password?token=${encodeURIComponent(token.plainTextToken)}`, - }, - } - }, - }, -}) - -await notifyUsing() - .channel('email', { email: 'ava@example.com', name: 'Ava' }) - .notify(passwordResetRequested(token)) -``` +- auth creates the token +- core builds the reset URL automatically from `APP_URL` and the configured broker route +- notifications or direct mail deliver the message when those integrations are installed If `@holo-js/auth` and `@holo-js/notifications` are both installed, core bridges the built-in auth delivery hook through notifications automatically. If notifications are absent but `@holo-js/mail` is installed, core falls back to direct mail delivery. + +Applications do not need to manually create `reset-password?token=...` URLs in normal usage. diff --git a/apps/docs/docs/security.md b/apps/docs/docs/security.md index 02f39ab..09f2726 100644 --- a/apps/docs/docs/security.md +++ b/apps/docs/docs/security.md @@ -113,6 +113,12 @@ Holo-JS falls back to standalone `host`, which may also be a Unix socket path. When `@holo-js/forms` is installed, forms can opt into security directly through `validate(...)`. +Validation failures and auth failures stay separate: + +- `validate(...)` returns form validation failures such as missing fields, bad formats, CSRF errors, and throttling. +- `login(...)`, `register(...)`, `verification.consume(...)`, `requestPasswordReset(...)`, and `resetPassword(...)` return auth failures in `error`. +- Auth failures are plain data with `status` and `fields`, so routes can forward them directly into the normal form response shape. + ### Login ```ts @@ -136,7 +142,18 @@ export async function POST(request: Request) { }) } - await login(submission.data) + const { error } = await login(submission.data) + if (error) { + return Response.json({ + ok: false, + status: error.status, + valid: false, + values: submission.values, + errors: error.fields, + }, { + status: error.status, + }) + } return Response.json(submission.success({ message: 'Logged in.', @@ -169,7 +186,18 @@ export async function POST(request: Request) { }) } - await register(submission.data) + const { error } = await register(submission.data) + if (error) { + return Response.json({ + ok: false, + status: error.status, + valid: false, + values: submission.values, + errors: error.fields, + }, { + status: error.status, + }) + } return Response.json(submission.success({ message: 'Account created.', diff --git a/bun.lock b/bun.lock index 86bc2ff..d8fbba9 100644 --- a/bun.lock +++ b/bun.lock @@ -54,7 +54,7 @@ "@holo-js/storage": "^0.1.4", "@holo-js/storage-s3": "^0.1.4", "esbuild": "^0.27.4", - "next": "^16.0.0", + "next": "catalog:", "react": "^19.0.0", "react-dom": "^19.0.0", }, @@ -134,7 +134,7 @@ "@holo-js/storage": "^0.1.4", "@holo-js/validation": "workspace:*", "esbuild": "^0.25.0", - "next": "^16.0.0", + "next": "catalog:", "react": "^19.0.0", "react-dom": "^19.0.0", }, @@ -177,14 +177,13 @@ "@holo-js/storage": "^0.1.4", "@holo-js/validation": "workspace:*", "esbuild": "^0.25.0", - "nuxt": "^4.0.0", + "nuxt": "catalog:", "vue": "^3.5.13", "vue-router": "^5.0.4", }, "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.8.0", - "vite": "^5.4.14", "vue-tsc": "^2.2.0", }, }, @@ -219,12 +218,12 @@ "@holo-js/session": "^0.1.4", "@holo-js/storage": "^0.1.4", "@holo-js/validation": "workspace:*", - "@sveltejs/adapter-node": "^5.0.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@sveltejs/adapter-node": "catalog:", + "@sveltejs/kit": "catalog:", + "@sveltejs/vite-plugin-svelte": "catalog:", "esbuild": "^0.25.0", - "svelte": "^5.0.0", - "vite": "^5.0.0", + "svelte": "catalog:", + "vite": "catalog:", }, "devDependencies": { "@types/node": "^22.0.0", @@ -268,12 +267,12 @@ "@holo-js/session": "^0.1.4", "@holo-js/storage": "^0.1.4", "@holo-js/storage-s3": "^0.1.4", - "@sveltejs/adapter-node": "^5.0.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@sveltejs/adapter-node": "catalog:", + "@sveltejs/kit": "catalog:", + "@sveltejs/vite-plugin-svelte": "catalog:", "esbuild": "^0.27.4", - "svelte": "^5.0.0", - "vite": "^5.0.0", + "svelte": "catalog:", + "vite": "catalog:", }, "devDependencies": { "@types/node": "^22.10.2", @@ -307,13 +306,13 @@ "@holo-js/config": "^0.1.4", "@holo-js/core": "^0.1.4", "@holo-js/db": "^0.1.4", - "@nuxt/kit": "^4.0.0", + "@nuxt/kit": "catalog:", }, "devDependencies": { "@holo-js/storage-s3": "workspace:*", "@nuxt/module-builder": "^1.0.2", "@types/node": "^22.10.2", - "nuxt": "^4.0.0", + "nuxt": "catalog:", "typescript": "^5.7.2", "vitest": "^2.1.8", }, @@ -334,6 +333,7 @@ "dependencies": { "@holo-js/config": "^0.1.4", "@holo-js/core": "^0.1.4", + "svelte": "catalog:", }, "devDependencies": { "@types/node": "^22.10.2", @@ -682,8 +682,8 @@ "version": "0.1.4", "dependencies": { "inflection": "catalog:", - "ulid": "^3.0.1", - "uuid": "^12.0.0", + "ulid": "catalog:", + "uuid": "catalog:", }, "devDependencies": { "@holo-js/db-mysql": "workspace:*", @@ -1068,8 +1068,11 @@ ], "catalog": { "@eslint/js": "^9.17.0", - "@nuxt/kit": "^4.0.0", + "@nuxt/kit": "^4.4.4", "@nuxt/module-builder": "^1.0.2", + "@sveltejs/adapter-node": "^5.5.4", + "@sveltejs/kit": "^2.59.1", + "@sveltejs/vite-plugin-svelte": "^7.1.0", "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.10.2", "@types/pg": "^8.11.0", @@ -1081,15 +1084,17 @@ "inflection": "^3.0.2", "ioredis": "^5.4.2", "mysql2": "^3.17.1", - "nuxt": "^4.0.0", + "next": "^16.2.4", + "nuxt": "^4.4.4", "pg": "^8.13.0", + "svelte": "^5.55.5", "tslib": "^2.8.1", "tsup": "^8.3.5", "typescript": "^5.7.2", "typescript-eslint": "^8.30.1", "ulid": "^3.0.1", "uuid": "^12.0.0", - "vite": "^5.4.14", + "vite": "^8.0.10", "vitepress": "^1.6.3", "vitest": "^2.1.8", "vue": "^3.5.13", @@ -1233,21 +1238,23 @@ "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], + "@colordx/core": ["@colordx/core@5.4.3", "", {}, "sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ=="], + "@docsearch/css": ["@docsearch/css@3.8.2", "", {}, "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ=="], "@docsearch/js": ["@docsearch/js@3.8.2", "", { "dependencies": { "@docsearch/react": "3.8.2", "preact": "^10.0.0" } }, "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ=="], "@docsearch/react": ["@docsearch/react@3.8.2", "", { "dependencies": { "@algolia/autocomplete-core": "1.17.7", "@algolia/autocomplete-preset-algolia": "1.17.7", "@docsearch/css": "3.8.2", "algoliasearch": "^5.14.2" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 19.0.0", "react": ">= 16.8.0 < 19.0.0", "react-dom": ">= 16.8.0 < 19.0.0", "search-insights": ">= 1 < 3" }, "optionalPeers": ["@types/react", "react", "react-dom", "search-insights"] }, "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg=="], - "@dxup/nuxt": ["@dxup/nuxt@0.4.0", "", { "dependencies": { "@dxup/unimport": "^0.1.2", "@nuxt/kit": "^4.2.2", "chokidar": "^5.0.0", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" }, "peerDependencies": { "typescript": "*" } }, "sha512-28LDotpr9G2knUse3cQYsOo6NJq5yhABv4ByRVRYJUmzf9Q31DI7rpRek4POlKy1aAcYyKgu5J2616pyqLohYg=="], + "@dxup/nuxt": ["@dxup/nuxt@0.4.1", "", { "dependencies": { "@dxup/unimport": "^0.1.2", "@nuxt/kit": "^4.4.2", "chokidar": "^5.0.0", "pathe": "^2.0.3", "tinyglobby": "^0.2.16" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-gtYffW6OfWNvoLW+XD3Mx/K8uUq08PMGLYJoDxc92EzZAWqR0FhcR5iaLm5r/OxyGTKz+P5f5Y7Aoir9+SjYaw=="], "@dxup/unimport": ["@dxup/unimport@0.1.2", "", {}, "sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ=="], - "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="], @@ -1509,25 +1516,25 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@next/env": ["@next/env@16.2.2", "", {}, "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ=="], + "@next/env": ["@next/env@16.2.4", "", {}, "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1545,139 +1552,139 @@ "@nuxt/devtools-wizard": ["@nuxt/devtools-wizard@3.2.4", "", { "dependencies": { "@clack/prompts": "^1.1.0", "consola": "^3.4.2", "diff": "^8.0.3", "execa": "^8.0.1", "magicast": "^0.5.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "semver": "^7.7.4" }, "bin": { "devtools-wizard": "cli.mjs" } }, "sha512-5tu2+Quu9XTxwtpzM8CUN0UKn/bzZIfJcoGd+at5Yy1RiUQJ4E52tRK0idW1rMSUDkbkvX3dSnu8Tpj7SAtWdQ=="], - "@nuxt/kit": ["@nuxt/kit@4.4.2", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "mlly": "^1.8.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-5+IPRNX2CjkBhuWUwz0hBuLqiaJPRoKzQ+SvcdrQDbAyE+VDeFt74VpSFr5/R0ujrK4b+XnSHUJWdS72w6hsog=="], + "@nuxt/kit": ["@nuxt/kit@4.4.4", "", { "dependencies": { "c12": "^3.3.4", "consola": "^3.4.2", "defu": "^6.1.7", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "mlly": "^1.8.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.1", "rc9": "^3.0.1", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.16", "ufo": "^1.6.4", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-oy4fAeMkyz7gelnalDQLPm8QZRN+c5c/Eh/M6oFgPx86jnA8m6xeOlONpJN2dk0GhcJwJYuN/kmzBffZ93WXPQ=="], "@nuxt/module-builder": ["@nuxt/module-builder@1.0.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "defu": "^6.1.4", "jiti": "^2.5.1", "magic-regexp": "^0.10.0", "mkdist": "^2.3.0", "mlly": "^1.7.4", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "tsconfck": "^3.1.6", "unbuild": "^3.6.0", "vue-sfc-transformer": "^0.1.16" }, "peerDependencies": { "@nuxt/cli": "^3.26.4", "typescript": "^5.8.3" }, "bin": { "nuxt-build-module": "dist/cli.mjs", "nuxt-module-build": "dist/cli.mjs" } }, "sha512-9M+0oZimbwom1J+HrfDuR5NDPED6C+DlM+2xfXju9wqB6VpVfYkS6WNEmS0URw8kpJcKBuogAc7ADO7vRS4s4A=="], - "@nuxt/nitro-server": ["@nuxt/nitro-server@4.4.2", "", { "dependencies": { "@babel/plugin-syntax-typescript": "^7.28.6", "@nuxt/devalue": "^2.0.2", "@nuxt/kit": "4.4.2", "@unhead/vue": "^2.1.12", "@vue/shared": "^3.5.30", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "devalue": "^5.6.3", "errx": "^0.1.0", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "h3": "^1.15.6", "impound": "^1.1.5", "klona": "^2.0.6", "mocked-exports": "^0.1.1", "nitropack": "^2.13.1", "nypm": "^0.6.5", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rou3": "^0.8.1", "std-env": "^4.0.0", "ufo": "^1.6.3", "unctx": "^2.5.0", "unstorage": "^1.17.4", "vue": "^3.5.30", "vue-bundle-renderer": "^2.2.0", "vue-devtools-stub": "^0.1.0" }, "peerDependencies": { "@babel/plugin-proposal-decorators": "^7.25.0", "@rollup/plugin-babel": "^6.0.0 || ^7.0.0", "nuxt": "^4.4.2" }, "optionalPeers": ["@babel/plugin-proposal-decorators", "@rollup/plugin-babel"] }, "sha512-iMTfraWcpA0MuEnnEI8JFK/4DODY4ss1CfB8m3sBVOqW9jpY1Z6hikxzrtN+CadtepW2aOI5d8TdX5hab+Sb4Q=="], + "@nuxt/nitro-server": ["@nuxt/nitro-server@4.4.4", "", { "dependencies": { "@babel/plugin-syntax-typescript": "^7.28.6", "@nuxt/devalue": "^2.0.2", "@nuxt/kit": "4.4.4", "@unhead/vue": "^2.1.13", "@vue/shared": "^3.5.33", "consola": "^3.4.2", "defu": "^6.1.7", "destr": "^2.0.5", "devalue": "^5.7.1", "errx": "^0.1.0", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "h3": "^1.15.11", "impound": "^1.1.5", "klona": "^2.0.6", "mocked-exports": "^0.1.1", "nitropack": "^2.13.4", "nypm": "^0.6.6", "ohash": "^2.0.11", "pathe": "^2.0.3", "rou3": "^0.8.1", "std-env": "^4.1.0", "ufo": "^1.6.4", "unctx": "^2.5.0", "unstorage": "^1.17.5", "vue": "^3.5.33", "vue-bundle-renderer": "^2.2.0", "vue-devtools-stub": "^0.1.0" }, "peerDependencies": { "@babel/plugin-proposal-decorators": "^7.25.0", "@rollup/plugin-babel": "^6.0.0 || ^7.0.0", "nuxt": "^4.4.4" }, "optionalPeers": ["@babel/plugin-proposal-decorators", "@rollup/plugin-babel"] }, "sha512-jMZPf+vJ2/IF5TZc+c/1c6O6p94pklVLvrexCu9FYZFK3H9oqYUlzBfYRd2kL5tdRTkIOpxTjfcgB1oc62UOhw=="], - "@nuxt/schema": ["@nuxt/schema@4.4.2", "", { "dependencies": { "@vue/shared": "^3.5.30", "defu": "^6.1.4", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "std-env": "^4.0.0" } }, "sha512-/q6C7Qhiricgi+PKR7ovBnJlKTL0memCbA1CzRT+itCW/oeYzUfeMdQ35mGntlBoyRPNrMXbzuSUhfDbSCU57w=="], + "@nuxt/schema": ["@nuxt/schema@4.4.4", "", { "dependencies": { "@vue/shared": "^3.5.33", "defu": "^6.1.7", "pathe": "^2.0.3", "pkg-types": "^2.3.1", "std-env": "^4.1.0" } }, "sha512-X70+lDZ4Wtp38l18/zFlKOZO5fd0uWQ60nrr1gxTNua8sqOxqVeZpLWTBmor7lFfJsXPPclsaFjcstyXYqXgpg=="], - "@nuxt/telemetry": ["@nuxt/telemetry@2.7.0", "", { "dependencies": { "citty": "^0.2.0", "consola": "^3.4.2", "ofetch": "^2.0.0-alpha.3", "rc9": "^3.0.0", "std-env": "^3.10.0" }, "peerDependencies": { "@nuxt/kit": ">=3.0.0" }, "bin": { "nuxt-telemetry": "bin/nuxt-telemetry.mjs" } }, "sha512-mrKC3NjAlBOooLLVTYcIUie1meipoYq5vkoESoVTEWTB34T3a0QJzOfOPch+HYlUR+5Lqy1zLMv6epHFgYAKLA=="], + "@nuxt/telemetry": ["@nuxt/telemetry@2.8.0", "", { "dependencies": { "citty": "^0.2.1", "consola": "^3.4.2", "ofetch": "^2.0.0-alpha.3", "rc9": "^3.0.0", "std-env": "^4.0.0" }, "peerDependencies": { "@nuxt/kit": ">=3.0.0" }, "bin": { "nuxt-telemetry": "bin/nuxt-telemetry.mjs" } }, "sha512-zAwXY24KYvpLTmiV+osagd2EHkfs5IF+7oDZYTQoit5r0kPlwaCNlzHp5I/wUAWT4LBw6lG8gZ6bWidAdv/erQ=="], - "@nuxt/vite-builder": ["@nuxt/vite-builder@4.4.2", "", { "dependencies": { "@nuxt/kit": "4.4.2", "@rollup/plugin-replace": "^6.0.3", "@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue-jsx": "^5.1.4", "autoprefixer": "^10.4.27", "consola": "^3.4.2", "cssnano": "^7.1.3", "defu": "^6.1.4", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "get-port-please": "^3.2.0", "jiti": "^2.6.1", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.1", "mocked-exports": "^0.1.1", "nypm": "^0.6.5", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "postcss": "^8.5.8", "seroval": "^1.5.1", "std-env": "^4.0.0", "ufo": "^1.6.3", "unenv": "^2.0.0-rc.24", "vite": "^7.3.1", "vite-node": "^5.3.0", "vite-plugin-checker": "^0.12.0", "vue-bundle-renderer": "^2.2.0" }, "peerDependencies": { "@babel/plugin-proposal-decorators": "^7.25.0", "@babel/plugin-syntax-jsx": "^7.25.0", "nuxt": "4.4.2", "rolldown": "^1.0.0-beta.38", "rollup-plugin-visualizer": "^6.0.0 || ^7.0.1", "vue": "^3.3.4" }, "optionalPeers": ["@babel/plugin-proposal-decorators", "@babel/plugin-syntax-jsx", "rolldown", "rollup-plugin-visualizer"] }, "sha512-fJaIwMA8ID6BU5EqmoDvnhq4qYDJeWjdHk4jfqy8D3Nm7CoUW0BvX7Ee92XoO05rtUiClGlk/NQ1Ii8hs3ZIbw=="], + "@nuxt/vite-builder": ["@nuxt/vite-builder@4.4.4", "", { "dependencies": { "@nuxt/kit": "4.4.4", "@rollup/plugin-replace": "^6.0.3", "@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue-jsx": "^5.1.5", "autoprefixer": "^10.5.0", "consola": "^3.4.2", "cssnano": "^7.1.7", "defu": "^6.1.7", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "get-port-please": "^3.2.0", "jiti": "^2.6.1", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.2", "mocked-exports": "^0.1.1", "nypm": "^0.6.6", "pathe": "^2.0.3", "pkg-types": "^2.3.1", "postcss": "^8.5.12", "seroval": "^1.5.2", "std-env": "^4.1.0", "ufo": "^1.6.4", "unenv": "^2.0.0-rc.24", "vite": "^7.3.2", "vite-node": "^5.3.0", "vite-plugin-checker": "^0.13.0", "vue-bundle-renderer": "^2.2.0" }, "peerDependencies": { "@babel/plugin-proposal-decorators": "^7.25.0", "@babel/plugin-syntax-jsx": "^7.25.0", "nuxt": "4.4.4", "rolldown": "^1.0.0-beta.38", "rollup-plugin-visualizer": "^6.0.0 || ^7.0.1", "vue": "^3.3.4" }, "optionalPeers": ["@babel/plugin-proposal-decorators", "@babel/plugin-syntax-jsx", "rolldown", "rollup-plugin-visualizer"] }, "sha512-SNyxEYVeTo3d26tt5rxS550VOFLyXx1UBqhZJexWhk42HgHa3d115LWZx+4e+FJf75SYZ1B/KTrkVeeOhfNBMw=="], - "@oxc-minify/binding-android-arm-eabi": ["@oxc-minify/binding-android-arm-eabi@0.117.0", "", { "os": "android", "cpu": "arm" }, "sha512-5Hf2KsGRjxp3HnPU/mse7cQJa5tWfMFUPZQcgSMVsv2JZnGFFOIDzA0Oja2KDD+VPJqMpEJKc2dCHAGZgJxsGg=="], + "@oxc-minify/binding-android-arm-eabi": ["@oxc-minify/binding-android-arm-eabi@0.128.0", "", { "os": "android", "cpu": "arm" }, "sha512-EwdDhZLRmXxSnfy0v9gdOru7TutM8ItRg1Xv8e2B4boWMnHlFCIH38JfwgQnenbkF8SVTwVJtDCkmwEzN4q3xA=="], - "@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.117.0", "", { "os": "android", "cpu": "arm64" }, "sha512-uuxGwxA5J4Sap+gz4nxyM/rer6q2A4X1Oe8HpE0CZQyb5cSBULQ15btZiVG3xOBctI5O+c2dwR1aZAP4oGKcLw=="], + "@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.128.0", "", { "os": "android", "cpu": "arm64" }, "sha512-kwJ8YxWTzty8hD36jXxKiB+Po/ecmHZvT1xAYklkATbr0A4NUqV32sV+3Wfm8TecdA6jX34/mc+4CKK2+Hha2Q=="], - "@oxc-minify/binding-darwin-arm64": ["@oxc-minify/binding-darwin-arm64@0.117.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lLBf75cxUSLydumToKtGTwbLqO/1urScblJ33Vx0uF38M2ZbL2x51AybBV5vlfLjYNrxvQ8ov0Bj/OhsVO/biA=="], + "@oxc-minify/binding-darwin-arm64": ["@oxc-minify/binding-darwin-arm64@0.128.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WBV8j5EZ7/3rvFbiJ8LxowmobR/XH+l2iRzkE7zRYLD5VC+TvZayYGrVGGDXQvXm6cGED0B1NweByTmeT4lpGQ=="], - "@oxc-minify/binding-darwin-x64": ["@oxc-minify/binding-darwin-x64@0.117.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-wBWwP1voLZMuN4hpe1HRtkPBd4/o/1qan5XssmmI/hewBvGHEHkyvVLS0zu+cKqXDxYzYvb/p+EqU+xSXhEl4A=="], + "@oxc-minify/binding-darwin-x64": ["@oxc-minify/binding-darwin-x64@0.128.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U4k1CSBsY1uf6yHE+vCNJp0mHzjsUUXgOZXMyhRN3sE2ovBDT9Gl8oACmLWPjg0R68jwP+1vhnNPsSqpTEOycg=="], - "@oxc-minify/binding-freebsd-x64": ["@oxc-minify/binding-freebsd-x64@0.117.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-pYSacHw698oH2vb70iP1cHk6x0zhvAuOvdskvNtEqvfziu8MSjKXa699vA9Cx72+DH5rwVuj1I3f+7no2fWglA=="], + "@oxc-minify/binding-freebsd-x64": ["@oxc-minify/binding-freebsd-x64@0.128.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-NT1GtcWpX4sOuU5dMdSNpdXJRpk9BGAHHnKc42IUId8E+jEhZUrg9vqIRIlspZG5O9Y7FjO2r6GBK93bpyIIUg=="], - "@oxc-minify/binding-linux-arm-gnueabihf": ["@oxc-minify/binding-linux-arm-gnueabihf@0.117.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ugm4Qj7F2+bccjhHCjjnSNHBDPyvjPXWrntID4WJpSrPqt+Az/o0EGdty9sWOjQXRZiTVpa80uqCWZQUn94yTA=="], + "@oxc-minify/binding-linux-arm-gnueabihf": ["@oxc-minify/binding-linux-arm-gnueabihf@0.128.0", "", { "os": "linux", "cpu": "arm" }, "sha512-OskPMYMH2KtkqvRMULF2/+55hFo/qmRz2p/g7Cp7XNiqdjZ/DvQDiVbME63rVoX3dYjgS15DolGbo54mHTyA9w=="], - "@oxc-minify/binding-linux-arm-musleabihf": ["@oxc-minify/binding-linux-arm-musleabihf@0.117.0", "", { "os": "linux", "cpu": "arm" }, "sha512-qrY6ZviO9wVRI/jl4nRZO4B9os8jaJQemMeWIyFInZNk3lhqihId8iBqMKibJnRaf+JRxLM9j68atXkFRhOHrg=="], + "@oxc-minify/binding-linux-arm-musleabihf": ["@oxc-minify/binding-linux-arm-musleabihf@0.128.0", "", { "os": "linux", "cpu": "arm" }, "sha512-fKUY7Y1vb8CYlGnS5FzqTeeM5zQz1Fleyaqz/T9iNHYAYNJ0Os9iT0rACLfAVCQKP9yOqPSwZ80xgZdVVGD61w=="], - "@oxc-minify/binding-linux-arm64-gnu": ["@oxc-minify/binding-linux-arm64-gnu@0.117.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-2VLJHKEFBRhCihT/8uesuDPhXpbWu1OlHCxqQ7pdFVqKik1Maj5E9oSDcYzxqfaCRStvTHkmLVWJBK5CVcIadg=="], + "@oxc-minify/binding-linux-arm64-gnu": ["@oxc-minify/binding-linux-arm64-gnu@0.128.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-T+CQQZ3BoWY/TxQk9LZsXZYj3madR/5tCErV6wzphTYZJfVjvKmQxnxMaT+TKE40Jha6+iGgwzxwcYWJfltULQ=="], - "@oxc-minify/binding-linux-arm64-musl": ["@oxc-minify/binding-linux-arm64-musl@0.117.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-C3zapJconWpl2Y7LR3GkRkH6jxpuV2iVUfkFcHT5Ffn4Zu7l88mZa2dhcfdULZDybN1Phka/P34YUzuskUUrXw=="], + "@oxc-minify/binding-linux-arm64-musl": ["@oxc-minify/binding-linux-arm64-musl@0.128.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-F6RkJ90S1Xt25Mk7/wPUmddsE4RZ7Nei+HlEa2FAjfhpoaTciOwV6E/Gtp7wPIYbwft7UfhMYwuEuZiZQytVWw=="], - "@oxc-minify/binding-linux-ppc64-gnu": ["@oxc-minify/binding-linux-ppc64-gnu@0.117.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2T/Bm+3/qTfuNS4gKSzL8qbiYk+ErHW2122CtDx+ilZAzvWcJ8IbqdZIbEWOlwwe03lESTxPwTBLFqVgQU2OeQ=="], + "@oxc-minify/binding-linux-ppc64-gnu": ["@oxc-minify/binding-linux-ppc64-gnu@0.128.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-0HP2FBGMlquLjShIIJvS4cebc6sdRRYL04GtxVpg96MtpejrkHYI2gQWcezsTUaGgg+eNRsuv2tdZPENu5+iWA=="], - "@oxc-minify/binding-linux-riscv64-gnu": ["@oxc-minify/binding-linux-riscv64-gnu@0.117.0", "", { "os": "linux", "cpu": "none" }, "sha512-MKLjpldYkeoB4T+yAi4aIAb0waifxUjLcKkCUDmYAY3RqBJTvWK34KtfaKZL0IBMIXfD92CbKkcxQirDUS9Xcg=="], + "@oxc-minify/binding-linux-riscv64-gnu": ["@oxc-minify/binding-linux-riscv64-gnu@0.128.0", "", { "os": "linux", "cpu": "none" }, "sha512-2j6Bd340IZqZbu4KUI28z87Ao9aHhq56HH1Qz5/+EdE732ajFYIoDF3z+QcxHXY0CFOG/Ur1ZOKTBEIWQ6BYIw=="], - "@oxc-minify/binding-linux-riscv64-musl": ["@oxc-minify/binding-linux-riscv64-musl@0.117.0", "", { "os": "linux", "cpu": "none" }, "sha512-UFVcbPvKUStry6JffriobBp8BHtjmLLPl4bCY+JMxIn/Q3pykCpZzRwFTcDurG/kY8tm+uSNfKKdRNa5Nh9A7g=="], + "@oxc-minify/binding-linux-riscv64-musl": ["@oxc-minify/binding-linux-riscv64-musl@0.128.0", "", { "os": "linux", "cpu": "none" }, "sha512-z5HSppdxNwB6//3Eo7mDWbTrLeyuTKvL/iLXaKEgocrJg1MhZLbRR7P5ore9gKvS4lF4EtEpA24xzilFxQK0iw=="], - "@oxc-minify/binding-linux-s390x-gnu": ["@oxc-minify/binding-linux-s390x-gnu@0.117.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-B9GyPQ1NKbvpETVAMyJMfRlD3c6UJ7kiuFUAlx9LTYiQL+YIyT6vpuRlq1zgsXxavZluVrfeJv6x0owV4KDx4Q=="], + "@oxc-minify/binding-linux-s390x-gnu": ["@oxc-minify/binding-linux-s390x-gnu@0.128.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-9rxYqH7P8NiYqRlLxlnNjJSF8BYADOmihM5ZHVkmlE4tqjHkoLNevdAyAP2ZBkL8QJflm1WGOXFWmFnWA54EvA=="], - "@oxc-minify/binding-linux-x64-gnu": ["@oxc-minify/binding-linux-x64-gnu@0.117.0", "", { "os": "linux", "cpu": "x64" }, "sha512-fXfhtr+WWBGNy4M5GjAF5vu/lpulR4Me34FjTyaK9nDrTZs7LM595UDsP1wliksqp4hD/KdoqHGmbCrC+6d4vA=="], + "@oxc-minify/binding-linux-x64-gnu": ["@oxc-minify/binding-linux-x64-gnu@0.128.0", "", { "os": "linux", "cpu": "x64" }, "sha512-sy5+4Oamw6Ly5gUNUIDQ7346Lryt7AhqjKhOtWl5dzYZnTIwwoI0V2DeIl3bR/vU8D629ZMYQOqhquRtSyBUOA=="], - "@oxc-minify/binding-linux-x64-musl": ["@oxc-minify/binding-linux-x64-musl@0.117.0", "", { "os": "linux", "cpu": "x64" }, "sha512-jFBgGbx1oLadb83ntJmy1dWlAHSQanXTS21G4PgkxyONmxZdZ/UMKr7KsADzMuoPsd2YhJHxzRpwJd9U+4BFBw=="], + "@oxc-minify/binding-linux-x64-musl": ["@oxc-minify/binding-linux-x64-musl@0.128.0", "", { "os": "linux", "cpu": "x64" }, "sha512-59Cxvjppy09TsaB15gr6rA9Bf87rm9t0bD1EW9dCZsdxWElnAC+TvWZ7v9dFUIeYeZUkhAAMPtpdqa3Y9CI2zA=="], - "@oxc-minify/binding-openharmony-arm64": ["@oxc-minify/binding-openharmony-arm64@0.117.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nxPd9vx1vYz8IlIMdl9HFdOK/ood1H5hzbSFsyO8JU55tkcJoBL8TLCbuFf9pHpOy27l2gcPyV6z3p4eAcTH5Q=="], + "@oxc-minify/binding-openharmony-arm64": ["@oxc-minify/binding-openharmony-arm64@0.128.0", "", { "os": "none", "cpu": "arm64" }, "sha512-XGa03zmiYpD7Kf1aXy6vjgkjfaCR90qH0TzGplnUXo6FF6gNe6sH9Zgneo9kxOyYt8CKKzXYD4VudT/nDTXq8Q=="], - "@oxc-minify/binding-wasm32-wasi": ["@oxc-minify/binding-wasm32-wasi@0.117.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-pSvjJ6cCCfEXSteWSiVfZhdRzvpmS3tLhlXrXTYiuTDFrkRCobRP39SRwAzK23rE9i/m2JAaES2xPEW6+xu85g=="], + "@oxc-minify/binding-wasm32-wasi": ["@oxc-minify/binding-wasm32-wasi@0.128.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-W+fK3cWhu/cUgx3NIAmDYcAyJs01aULlr3E3n/ZN79Q1/CX+FS+yWfwt/IysIi4FhpVL7z58azbJHDzhEx4X4g=="], - "@oxc-minify/binding-win32-arm64-msvc": ["@oxc-minify/binding-win32-arm64-msvc@0.117.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-9NoT9baFrWPdJRIZVQ1jzPZW9TjPT2sbzQyDdoK7uD1V8JXCe1L2y7sp9k2ldZZheaIcmtNwHc7jyD7kYz/0XQ=="], + "@oxc-minify/binding-win32-arm64-msvc": ["@oxc-minify/binding-win32-arm64-msvc@0.128.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-pwMZd27FF+j4tHLYKtu4QBl6KI0gkt6xTNGLffs8VlH5vfDPHUvLo/AS6y66tdEjQ3chhs8OGg1mAFhPoQldDw=="], - "@oxc-minify/binding-win32-ia32-msvc": ["@oxc-minify/binding-win32-ia32-msvc@0.117.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-E51LTjkRei5u2dpFiYSObuh+e43xg45qlmilSTd0XDGFdYJCOv62Q0MEn61TR+efQYPNleYwWdTS9t+tp9p/4w=="], + "@oxc-minify/binding-win32-ia32-msvc": ["@oxc-minify/binding-win32-ia32-msvc@0.128.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-GskPdx/Fsn3ttkJbzxh51LYhla4N4p1sMufJKgf6PHupt5RukBaHI/GKM/2ni6ObxUI0b9UK37fROdV+5ekpMQ=="], - "@oxc-minify/binding-win32-x64-msvc": ["@oxc-minify/binding-win32-x64-msvc@0.117.0", "", { "os": "win32", "cpu": "x64" }, "sha512-I8vniPOxWQdxfIbXNvQLaJ1n8SrnqES6wuiAX10CU72sKsizkds9kDaJ1KzWvDy39RKhTBmD1cJsU2uxPFgizQ=="], + "@oxc-minify/binding-win32-x64-msvc": ["@oxc-minify/binding-win32-x64-msvc@0.128.0", "", { "os": "win32", "cpu": "x64" }, "sha512-m8oakspZCbCod3WuY0U9DvwQlhMYaU31bK+Way1Rb+JGs455WLtkebEie/luSuN5DeF+aZyRH/zt1AY4weKQQg=="], - "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.117.0", "", { "os": "android", "cpu": "arm" }, "sha512-XarGPJpaobgKjfm7xRfCGWWszuPbm/OeP91NdMhxtcLZ/qLTmWF0P0z0gqmr0Uysi1F1v1BNtcST11THMrcEOw=="], + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.128.0", "", { "os": "android", "cpu": "arm" }, "sha512-aca6ZvzmCBUGOANQRiRQRZuRKYI3ENhcit6GisnknOOmcezfQc7xJ4dxlPU7MV7mOvrC7RNR1u3LAD7xyaiCxA=="], - "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.117.0", "", { "os": "android", "cpu": "arm64" }, "sha512-EPTs2EBijGmyhPso4rXAL0NSpECXER9IaVKFZEv83YcA6h4uhKW47kmYt+OZcSp130zhHx+lTWILDQ/LDkCRNA=="], + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.128.0", "", { "os": "android", "cpu": "arm64" }, "sha512-BbeDmuohoJ7Rz/it5wnkj69i/OsCPS3Z51nLEzwO/Y6YshtC4JU+15oNwhY8v4LRKRYclRc7ggOikwrsJ/eOEQ=="], - "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.117.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3bAEpyih6r/Kb+Xzn1em1qBMClOS7NsVWgF86k95jpysR5ix/HlKFKSy7cax6PcS96HeHR4kjlME20n/XK1zNg=="], + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.128.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tRUHPt80417QmvNpoSslJT1VY8NUbWdrWR+L14Zn+RbOTcaqB8E6PYE/ZGN8jjWBzqporiA/H4MfO50ew/NCNA=="], - "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.117.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-W7S99zFwVZhSbCxvjfZkioStFU249DBc4TJw/kK6kfKwx2Zew+jvizX5Y3ZPkAh7fBVUSNOdSeOqLBHLiP50tw=="], + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.128.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-rWI2Hb1Nt3U/vKsjyNvZzDC8i/l144U20DKjhzaTmwIhIiSRGeroPWWiImwypmKLqrw8GuIixbWJkpGWLbkzrQ=="], - "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.117.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xH76lqSdjCSY0KUMPwLXlvQ3YEm3FFVEQmgiOCGNf+stZ6E4Mo3nC102Bo8yKd7aW0foIPAFLYsHgj7vVI/axw=="], + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.128.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hhpdVMaNCLgQxjgNPeeFzSeJMmZPc5lKfv0NGSI3egZq9EdnEGqeC8JsYsQjK7PoQgbvZ17xlj0SO5ziH5Obkg=="], - "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.117.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9Hdm1imzrn4RdMYnQKKcy+7p7QsSPIrgVIZmpGSJT02nYDuBWLdG1pdYMPFoEo46yiXry3tS3RoHIpNbT1IiyQ=="], + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.128.0", "", { "os": "linux", "cpu": "arm" }, "sha512-093zNw0zZ/e/obML+rhlSdmnzR0mVZluPcAkxunEc5E3F0yBVsFn24Y1ILfsEte11Ud041qn/gp2OJ1jxNqUng=="], - "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.117.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Itszer/VCeYhYVJLcuKnHktlY8QyGnVxapltP68S1XRGlV6IsM9HQAElJRMwQhT6/GkMjOhANmkv2Qu/9v44lw=="], + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.128.0", "", { "os": "linux", "cpu": "arm" }, "sha512-fq7DmKmfC+dvD97IXrgbph6Jzwe0EDu+PYMofmzZ6fv5X1k9vtaqLpDGMuICO9MmUnyKAQmVl+wIv2RNy4Dz8g=="], - "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.117.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jBxD7DtlHQ36ivjjZdH0noQJgWNouenzpLmXNKnYaCsBfo3jY95m5iyjYQEiWkvkhJ3TJUAs7tQ1/kEpY7x/Kg=="], + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.128.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Xvm48jJah8TlIrURIjNOP/gNiGe6aKvCB+r06VliflFo8Kq7VOLE8PxtgShJzZIqubrgdMdYfvuPPozn7F6MbQ=="], - "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.117.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-QagKTDF4lrz8bCXbUi39Uq5xs7C7itAseKm51f33U+Dyar9eJY/zGKqfME9mKLOiahX7Fc1J3xMWVS0AdDXLPg=="], + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.128.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-M7iwBGmYJTx+pKOYFjI0buop4gJvlmcVzFGaXPt21DKpQkbQZG1f63Yg7LloIYT/t9yLxCw0Lhfx/RFlAlMSjA=="], - "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.117.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RPddpcE/0xxWaommWy0c5i/JdrXcXAkxBS2GOrAUh5LKmyCh03hpJedOAWszG4ADsKQwoUQQ1/tZVGRhZIWtKA=="], + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.128.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-21LGNIZb1Pcfk5/EGsqabrxv4yqQOWis1407JJrClS7XpFCrbvr74YAB1V+m54cYbwvO6UWwQqS4WecxiyfCRg=="], - "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.117.0", "", { "os": "linux", "cpu": "none" }, "sha512-ur/WVZF9FSOiZGxyP+nfxZzuv6r5OJDYoVxJnUR7fM/hhXLh4V/be6rjbzm9KLCDBRwYCEKJtt+XXNccwd06IA=="], + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.128.0", "", { "os": "linux", "cpu": "none" }, "sha512-gyHjOTFpg9bTTYjxPmQirvufb89+VdZwVfcMtAUyPr6F5H8ZswvCQshK4qOW+Q+2Xyb33hduRgY/eFHJQjU/vQ=="], - "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.117.0", "", { "os": "linux", "cpu": "none" }, "sha512-ujGcAx8xAMvhy7X5sBFi3GXML1EtyORuJZ5z2T6UV3U416WgDX/4OCi3GnoteeenvxIf6JgP45B+YTHpt71vpA=="], + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.128.0", "", { "os": "linux", "cpu": "none" }, "sha512-X6Q2oKUrP5GyDd2xniuEBLk6aFQCZ97W2+aVXGgJXdjx5t4/oFuA9ri0wLOUrBIX+qdSuK581snMBio4z910eA=="], - "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.117.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hbsfKjUwRjcMZZvvmpZSc+qS0bHcHRu8aV/I3Ikn9BzOA0ZAgUE7ctPtce5zCU7bM8dnTLi4sJ1Pi9YHdx6Urw=="], + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.128.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-BdzTmqxfxoYkpgokoLaSnOX6T+R3/goL42klre2tnG+kHbG2TXS0VN+P5BPofH1axdKOHy5ei4ENZrjmCOt2lA=="], - "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.117.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1QrTrf8rige7UPJrYuDKJLQOuJlgkt+nRSJLBMHWNm9TdivzP48HaK3f4q18EjNlglKtn03lgjMu4fryDm8X4A=="], + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.128.0", "", { "os": "linux", "cpu": "x64" }, "sha512-OO1nW2Q7sSYYvJZpDHdvyFSdRaVcQqRijZSSmWVMqFxPYy8cEF45zJ9fcdIYuzIT3jYq6YRhEFm/VMWNWhE22Q=="], - "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.117.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gRvK6HPzF5ITRL68fqb2WYYs/hGviPIbkV84HWCgiJX+LkaOpp+HIHQl3zVZdyKHwopXToTbXbtx/oFjDjl8pg=="], + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.128.0", "", { "os": "linux", "cpu": "x64" }, "sha512-4NehAe404MRdoZVS9DW8C5XbJwbXIc/KfVlYdpi5vE4081zc9Y0YzKVqyOYj/Puye7/Do+ohaONBFWlEHYl9hw=="], - "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.117.0", "", { "os": "none", "cpu": "arm64" }, "sha512-QPJvFbnnDZZY7xc+xpbIBWLThcGBakwaYA9vKV8b3+oS5MGfAZUoTFJcix5+Zg2Ri46sOfrUim6Y6jsKNcssAQ=="], + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.128.0", "", { "os": "none", "cpu": "arm64" }, "sha512-kVbqgW9xLL8bh8oc7aYOJilRKXE5G33+tE0jan+duo/9OriaFRpijcCwT2waWs2oqYROYq0GlE7/p3ywoshVeg=="], - "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.117.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-+XRSNA0xt3pk/6CUHM7pykVe7M8SdifJk8LX1+fIp/zefvR3HBieZCbwG5un8gogNgh7srLycoh/cQA9uozv5g=="], + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.128.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-L38ojghJYHmgiz6fJd7jwLB/ESDBpB02NdFxh+smqVM6P2anCEvHn0jhaSrt5eVNR1Ak8+moOeftUlofeyvniA=="], - "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.117.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-GpxeGS+Vo030DsrXeRPc7OSJOQIyAHkM3mzwBcnQjg/79XnOIDDMXJ5X6/aNdkVt/+Pv35pqKzGA4TQau97x8w=="], + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.128.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-xgvO35GyHBtjlQ5AEpaYr7Rll1rvY7zqIhT6ty8E3ezBW2J1SFLjIDEvI/tcgDg6oaseDAqVcM+jU1HuCekgZw=="], - "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.117.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-tchWEYiso1+objTZirmlR+w3fcIel6PVBOJ8NuC2Jr30dxBOiKUfFLovJLANwHg1+TzeD6pVSLIIIEf2T5o5lQ=="], + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.128.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-OY+3eM2SN72prHKRB22mPz8o5A/7dJ+f5DFLBVvggyZhEaNDAH9IB+ElMjmOkOIwf5MDCUAowCK7pAncNxzpBA=="], - "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.117.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ysRJAjIbB4e5y+t9PZs7TwbgOV/GVT//s30AORLCT/pedYwpYzHq6ApXK7is9fvyfZtgT3anNir8+esurmyaDw=="], + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.128.0", "", { "os": "win32", "cpu": "x64" }, "sha512-NE9ny+cPUCCObXa0IKLfj0tCdPd7pe/dz9ZpkxpUOymB3miNeMPybdlYYTBSGJUalMWeBM85/4JcCErCNTqOXw=="], - "@oxc-project/types": ["@oxc-project/types@0.117.0", "", {}, "sha512-C/kPXBphID44fXdsa2xSOCuzX8fKZiFxPsvucJ6Yfkr6CJlMA+kNLPNKyLoI+l9XlDsNxBrz6h7IIjKU8pB69w=="], + "@oxc-project/types": ["@oxc-project/types@0.128.0", "", {}, "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ=="], - "@oxc-transform/binding-android-arm-eabi": ["@oxc-transform/binding-android-arm-eabi@0.117.0", "", { "os": "android", "cpu": "arm" }, "sha512-17giX7h5VR9Eodru4OoSCFdgwLFIaUxeEn8JWe0vMZrAuRbT9NiDTy5dXdbGQBoO8aXPkbGS38FGlvbi31aujw=="], + "@oxc-transform/binding-android-arm-eabi": ["@oxc-transform/binding-android-arm-eabi@0.128.0", "", { "os": "android", "cpu": "arm" }, "sha512-qVO4izEs88ZSo7KOK4P+O5nAXXJmkSadInvFjGkhVnm2R2Wr8trU/GLhjAK0S0u8Qv9bkXspNhgpECk+CTQ/ew=="], - "@oxc-transform/binding-android-arm64": ["@oxc-transform/binding-android-arm64@0.117.0", "", { "os": "android", "cpu": "arm64" }, "sha512-1LrDd1CPochtLx04pAafdah6QtOQQj0/Evttevi+0u8rCI5FKucIG7pqBHkIQi/y7pycFYIj+GebhET80maeUg=="], + "@oxc-transform/binding-android-arm64": ["@oxc-transform/binding-android-arm64@0.128.0", "", { "os": "android", "cpu": "arm64" }, "sha512-F3RXlbCzIgkpRWlz1PEguDZl5NzZRmbeHKTFTQWFnK6mIdw2EkWihPVv9+CIcO80c7+sF/YRGOBaji6hwUDhtQ=="], - "@oxc-transform/binding-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.117.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-K1Xo52xJOvFfHSkz2ax9X5Qsku23RCfTIPbHZWdUCAQ1TQooI+sFcewSubhVUJ4DVK12/tYT//XXboumin+FHA=="], + "@oxc-transform/binding-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.128.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xj63gIzQ67LDYHCOWXSHgfx4LbPVz1ck0G3y0eR6mbgYk3CwwylbhWi/CaDC6BWsHwoLQryeYjHB5XBCR0EPMQ=="], - "@oxc-transform/binding-darwin-x64": ["@oxc-transform/binding-darwin-x64@0.117.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ftFT/8Laolfq49mRRWLkIhd1AbJ0MI5bW3LwddvdoAg9zXwkx4qhzTYyBPRZhvXWftts+NjlHfHsXCOqI4tPtw=="], + "@oxc-transform/binding-darwin-x64": ["@oxc-transform/binding-darwin-x64@0.128.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-YQkvFqNqpwEt197RjREAOWeRANalPtCD+ayZlx4IjTQ6IOYZEP83B9/++gTQisHV3r8E7dU8UqJKeSS1cHlTQg=="], - "@oxc-transform/binding-freebsd-x64": ["@oxc-transform/binding-freebsd-x64@0.117.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QDRyw0atg9BMnwOwnJeW6REzWPLEjiWtsCc2Sj612F1hCdvP+n0L3o8sHinEWM+BiOkOYtUxHA69WjUslc3G+g=="], + "@oxc-transform/binding-freebsd-x64": ["@oxc-transform/binding-freebsd-x64@0.128.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Jvd3Ximb3x3o0+xRBB5lq63JlzxhJN787IsBjn0PEnmuocYQj+tJ5BB4n9xPIG27GXwg3ycckQPO/RsWeEcBPg=="], - "@oxc-transform/binding-linux-arm-gnueabihf": ["@oxc-transform/binding-linux-arm-gnueabihf@0.117.0", "", { "os": "linux", "cpu": "arm" }, "sha512-UvpvOjyQVgiIJahIpMT0qAsLJT8O1ibHTBgXGOsZkQgw1xmjARPQ07dpRcucPPn6cqCF3wrxfbqtr2vFHaMkdA=="], + "@oxc-transform/binding-linux-arm-gnueabihf": ["@oxc-transform/binding-linux-arm-gnueabihf@0.128.0", "", { "os": "linux", "cpu": "arm" }, "sha512-TaRKWeGnAJNIdCa5+m0I8/SksBgkLX94iH40qy3chvLuaIOGAmOViUStYx8geXBzO9P99V7En8nHXLoqCONBRQ=="], - "@oxc-transform/binding-linux-arm-musleabihf": ["@oxc-transform/binding-linux-arm-musleabihf@0.117.0", "", { "os": "linux", "cpu": "arm" }, "sha512-cIhztGFjKk8ngP+/7EPkEhzWMGr2neezxgWirSn/f/MirjH234oHHGJ2diKIbGQEsy0aOuJMTkL9NLfzfmH51A=="], + "@oxc-transform/binding-linux-arm-musleabihf": ["@oxc-transform/binding-linux-arm-musleabihf@0.128.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7TMrtA5/3SCvS+yMPrGnri5T4ZhIoCbjwKWV6Kn8d3v+vx7MpEmNkfe+CdF3rb5LlnuxeDMPwr1E2ntya0b8HQ=="], - "@oxc-transform/binding-linux-arm64-gnu": ["@oxc-transform/binding-linux-arm64-gnu@0.117.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-mXbDfvDN0RZVg7v4LohNzU0kK3fMAZgkUKTkpFVgxEvzibEG5VpSznkypUwHI4a8U8pz+K6mGaLetX3Xt+CvvA=="], + "@oxc-transform/binding-linux-arm64-gnu": ["@oxc-transform/binding-linux-arm64-gnu@0.128.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lMQEa1jLBNm1N+5uvyj9zX9urVY4xKkLnhO8/4CtSGdXX+mExqsVawyQPAZqbtq1fLQ0yt1QYJ9YuM0+fiSJTQ=="], - "@oxc-transform/binding-linux-arm64-musl": ["@oxc-transform/binding-linux-arm64-musl@0.117.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ykxpPQp0eAcSmhy0Y3qKvdanHY4d8THPonDfmCoktUXb6r0X6qnjpJB3V+taN1wevW55bOEZd97kxtjTKjqhmg=="], + "@oxc-transform/binding-linux-arm64-musl": ["@oxc-transform/binding-linux-arm64-musl@0.128.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-dPSjyd0gQ9dE3mpdJi0BHNJaqQz4V7mVW6Fbs6jRSiGnrxwGfXdMJFInXoJ49B3k5Zhfa9Is9Ixp6St7c6ouCA=="], - "@oxc-transform/binding-linux-ppc64-gnu": ["@oxc-transform/binding-linux-ppc64-gnu@0.117.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rvspti4Kr7eq6zSrURK5WjscfWQPvmy/KjJZV45neRKW8RLonE3r9+NgrwSLGoHvQ3F24fbqlkplox1RtlhH5A=="], + "@oxc-transform/binding-linux-ppc64-gnu": ["@oxc-transform/binding-linux-ppc64-gnu@0.128.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-YNa9XAotPKvAXFJrHC7kBsHMVg0HOB4vRiKuYUjzFsfLkxTbuztKHTKG/gW5kjp7dBw+TNFofTaVCVZgOnHXPQ=="], - "@oxc-transform/binding-linux-riscv64-gnu": ["@oxc-transform/binding-linux-riscv64-gnu@0.117.0", "", { "os": "linux", "cpu": "none" }, "sha512-Dr2ZW9ZZ4l1eQ5JUEUY3smBh4JFPCPuybWaDZTLn3ADZjyd8ZtNXEjeMT8rQbbhbgSL9hEgbwaqraole3FNThQ=="], + "@oxc-transform/binding-linux-riscv64-gnu": ["@oxc-transform/binding-linux-riscv64-gnu@0.128.0", "", { "os": "linux", "cpu": "none" }, "sha512-jjSiG9H8ya/U3igW5DdIBFIDwhffF7Vbc7th2tcHV73eg0DQz75n36a9RmQ1/0aS9vknUuNtY6SODr8/gmuzsQ=="], - "@oxc-transform/binding-linux-riscv64-musl": ["@oxc-transform/binding-linux-riscv64-musl@0.117.0", "", { "os": "linux", "cpu": "none" }, "sha512-oD1Bnes1bIC3LVBSrWEoSUBj6fvatESPwAVWfJVGVQlqWuOs/ZBn1e4Nmbipo3KGPHK7DJY75r/j7CQCxhrOFQ=="], + "@oxc-transform/binding-linux-riscv64-musl": ["@oxc-transform/binding-linux-riscv64-musl@0.128.0", "", { "os": "linux", "cpu": "none" }, "sha512-FVUr/XNT7BfQA4XVMel/HTCJi5mQyEitslgX42ztYPnCFMRFG1sQQKgnlLJdl7qifuyxpvKLR1f7h7HEuwWw1Q=="], - "@oxc-transform/binding-linux-s390x-gnu": ["@oxc-transform/binding-linux-s390x-gnu@0.117.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-qT//IAPLvse844t99Kff5j055qEbXfwzWgvCMb0FyjisnB8foy25iHZxZIocNBe6qwrCYWUP1M8rNrB/WyfS1Q=="], + "@oxc-transform/binding-linux-s390x-gnu": ["@oxc-transform/binding-linux-s390x-gnu@0.128.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-caJnVw5PG8v339zAyHgA7p34xXa3A4Kc9VyrDgsT1znr51qacaUv4BRlgRi0qkqxRWXYNVFfsbU2g0t1qS7E9w=="], - "@oxc-transform/binding-linux-x64-gnu": ["@oxc-transform/binding-linux-x64-gnu@0.117.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2YEO5X+KgNzFqRVO5dAkhjcI5gwxus4NSWVl/+cs2sI6P0MNPjqE3VWPawl4RTC11LvetiiZdHcujUCPM8aaUw=="], + "@oxc-transform/binding-linux-x64-gnu": ["@oxc-transform/binding-linux-x64-gnu@0.128.0", "", { "os": "linux", "cpu": "x64" }, "sha512-zkQKjsHEUX3ckQBcZTtHE/7pgFMkWQp6y/4t7N8eT3j8wnoL+vapv7l4ISjgx1/EePRJN1HErYXmriz7tPVKRg=="], - "@oxc-transform/binding-linux-x64-musl": ["@oxc-transform/binding-linux-x64-musl@0.117.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3wqWbTSaIFZvDr1aqmTul4cg8PRWYh6VC52E8bLI7ytgS/BwJLW+sDUU2YaGIds4sAf/1yKeJRmudRCDPW9INg=="], + "@oxc-transform/binding-linux-x64-musl": ["@oxc-transform/binding-linux-x64-musl@0.128.0", "", { "os": "linux", "cpu": "x64" }, "sha512-NjYtwl9ijp34iisHxYBvE7nii1Ac0QPP3doHv8MQHhDA3zjUcDCROuBNybfaEYCxnJ1aF+cAPqsyeopnAGsyuQ=="], - "@oxc-transform/binding-openharmony-arm64": ["@oxc-transform/binding-openharmony-arm64@0.117.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Ebxx6NPqhzlrjvx4+PdSqbOq+li0f7X59XtJljDghkbJsbnkHvhLmPR09ifHt5X32UlZN63ekjwcg/nbmHLLlA=="], + "@oxc-transform/binding-openharmony-arm64": ["@oxc-transform/binding-openharmony-arm64@0.128.0", "", { "os": "none", "cpu": "arm64" }, "sha512-itsi0tVkVdrYphSppdFChLq9tD0pvbRRS3EV8NQYKZ/NWHMoxzjlf9TFA/ZZYV113juYo1Dq3glVX48knhBeFQ=="], - "@oxc-transform/binding-wasm32-wasi": ["@oxc-transform/binding-wasm32-wasi@0.117.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-Nn8mmcBiQ0XKHLTb05QBlH+CDkn7jf5YDVv9FtKhy4zJT0NEU9y3dXVbfcurOpsVrG9me4ktzDQNCaAoJjUQyw=="], + "@oxc-transform/binding-wasm32-wasi": ["@oxc-transform/binding-wasm32-wasi@0.128.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-elzjX2gy1jcseeFaKtbk/6T2FPTpGNx0IpeD0iyk6cahWN7wD6eHY5u7th1X85cYbRq9rqniS+xYIxN3StthWg=="], - "@oxc-transform/binding-win32-arm64-msvc": ["@oxc-transform/binding-win32-arm64-msvc@0.117.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-15cbsF8diXWGnHrTsVgVeabETiT/KdMAfRAcot99xsaVecJs3pITNNjC6Qj+/TPNpehbgIFjlhhxOVSbQsTBgg=="], + "@oxc-transform/binding-win32-arm64-msvc": ["@oxc-transform/binding-win32-arm64-msvc@0.128.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-p5LmbI66dk2dziJSUzjQ24gOWeI6pJpXcOC6famloRtKCq54V5/KegsztFgZZCtYFEAEqFgcfspFHrV+CcKWcg=="], - "@oxc-transform/binding-win32-ia32-msvc": ["@oxc-transform/binding-win32-ia32-msvc@0.117.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-I6DkhCuFX6p9rckdWiLuZfBWrrYUC7sNX+zLaCfa5zvrPNwo1/29KkefvqXVxu3AWT/6oZAbtc0A8/mqhETJPQ=="], + "@oxc-transform/binding-win32-ia32-msvc": ["@oxc-transform/binding-win32-ia32-msvc@0.128.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-CMU3Yn05rXeLw7GyVlDB3bbp2iV14yt3VWyF0pNuMK9NVgOmUkXgFLe5SOcX9rEm64TRJjOMEghtE5+r0GtqIQ=="], - "@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.117.0", "", { "os": "win32", "cpu": "x64" }, "sha512-V7YzavQnYcRJBeJkp0qpb3FKrlm5I57XJetCYB4jsjStuboQmnFMZ/XQH55Szlf/kVyeU9ddQwv72gJJ5BrGjQ=="], + "@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.128.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Vck5AdNH2JPYMQWVDxvX5PbDFfqVG+tCOgKJzAC/S9bgbD3qcMjN5Dx6FOmEbwY3hZm//fzOsY4tErofoiK/aQ=="], "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], @@ -1719,7 +1726,37 @@ "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="], "@rollup/plugin-alias": ["@rollup/plugin-alias@5.1.1", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ=="], @@ -1815,11 +1852,9 @@ "@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.4", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.59.0" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ=="], - "@sveltejs/kit": ["@sveltejs/kit@2.55.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.4", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA=="], + "@sveltejs/kit": ["@sveltejs/kit@2.59.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.4", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-d8OON70AphLdDesuTIl//M2O6fRTIicX8aYv8vhCiYEhTTI2OboKqey0Hu1A4VFhqwgqtq0vKDmPFGkw8kKmgw=="], - "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@4.0.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", "debug": "^4.3.7", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.12", "vitefu": "^1.0.3" }, "peerDependencies": { "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA=="], - - "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@3.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ=="], + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@7.1.0", "", { "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.2" }, "peerDependencies": { "svelte": "^5.46.4", "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "sha512-atk0ZbHXKvUOpn8eG3kE0BvRcWvMubOYLLMvyLFYpf+IpLj7lfEaykMa/2Yy8o7PJ/qvxXO4SXruq8coIjq1yw=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], @@ -1887,7 +1922,7 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@unhead/vue": ["@unhead/vue@2.1.12", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.12" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-zEWqg0nZM8acpuTZE40wkeUl8AhIe0tU0OkilVi1D4fmVjACrwoh5HP6aNqJ8kUnKsoy6D+R3Vi/O+fmdNGO7g=="], + "@unhead/vue": ["@unhead/vue@2.1.13", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.13" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-HYy0shaHRnLNW9r85gppO8IiGz0ONWVV3zGdlT8CQ0tbTwixznJCIiyqV4BSV1aIF1jJIye0pd1p/k6Eab8Z/A=="], "@vercel/nft": ["@vercel/nft@1.5.0", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA=="], @@ -1929,11 +1964,11 @@ "@vue/compiler-core": ["@vue/compiler-core@3.5.31", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/shared": "3.5.31", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ=="], - "@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="], + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.33", "", { "dependencies": { "@vue/compiler-core": "3.5.33", "@vue/shared": "3.5.33" } }, "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA=="], - "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.31", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.31", "@vue/compiler-dom": "3.5.31", "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q=="], + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.33", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.33", "@vue/compiler-dom": "3.5.33", "@vue/compiler-ssr": "3.5.33", "@vue/shared": "3.5.33", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.10", "source-map-js": "^1.2.1" } }, "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA=="], - "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.33", "", { "dependencies": { "@vue/compiler-dom": "3.5.33", "@vue/shared": "3.5.33" } }, "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A=="], "@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="], @@ -1947,15 +1982,15 @@ "@vue/language-core": ["@vue/language-core@2.2.12", "", { "dependencies": { "@volar/language-core": "2.4.15", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA=="], - "@vue/reactivity": ["@vue/reactivity@3.5.31", "", { "dependencies": { "@vue/shared": "3.5.31" } }, "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g=="], + "@vue/reactivity": ["@vue/reactivity@3.5.33", "", { "dependencies": { "@vue/shared": "3.5.33" } }, "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A=="], - "@vue/runtime-core": ["@vue/runtime-core@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q=="], + "@vue/runtime-core": ["@vue/runtime-core@3.5.33", "", { "dependencies": { "@vue/reactivity": "3.5.33", "@vue/shared": "3.5.33" } }, "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ=="], - "@vue/runtime-dom": ["@vue/runtime-dom@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/runtime-core": "3.5.31", "@vue/shared": "3.5.31", "csstype": "^3.2.3" } }, "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g=="], + "@vue/runtime-dom": ["@vue/runtime-dom@3.5.33", "", { "dependencies": { "@vue/reactivity": "3.5.33", "@vue/runtime-core": "3.5.33", "@vue/shared": "3.5.33", "csstype": "^3.2.3" } }, "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw=="], - "@vue/server-renderer": ["@vue/server-renderer@3.5.31", "", { "dependencies": { "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31" }, "peerDependencies": { "vue": "3.5.31" } }, "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA=="], + "@vue/server-renderer": ["@vue/server-renderer@3.5.33", "", { "dependencies": { "@vue/compiler-ssr": "3.5.33", "@vue/shared": "3.5.33" }, "peerDependencies": { "vue": "3.5.33" } }, "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw=="], - "@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="], + "@vue/shared": ["@vue/shared@3.5.33", "", {}, "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ=="], "@vueuse/core": ["@vueuse/core@12.8.2", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" } }, "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ=="], @@ -2039,7 +2074,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.27", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA=="], "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], @@ -2077,7 +2112,7 @@ "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], - "c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="], + "c12": ["c12@3.3.4", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.4", "defu": "^6.1.6", "dotenv": "^17.3.1", "exsolve": "^1.0.8", "giget": "^3.2.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", "rc9": "^3.0.1" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], @@ -2085,7 +2120,7 @@ "caniuse-api": ["caniuse-api@3.0.0", "", { "dependencies": { "browserslist": "^4.0.0", "caniuse-lite": "^1.0.0", "lodash.memoize": "^4.1.2", "lodash.uniq": "^4.5.0" } }, "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw=="], - "caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001791", "", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -2145,7 +2180,7 @@ "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], - "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], + "cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="], "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="], @@ -2221,7 +2256,7 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "devalue": ["devalue@5.6.4", "", {}, "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA=="], + "devalue": ["devalue@5.8.0", "", {}, "sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], @@ -2333,6 +2368,12 @@ "fast-npm-meta": ["fast-npm-meta@1.4.2", "", { "bin": { "fast-npm-meta": "dist/cli.mjs" } }, "sha512-XXyd9d3ie/JeIIjm6WeKalvapGGFI4ShAjPJM78vgUFYzoEsuNSjvvVTuht0XZcwbVdOnEEGzhxwguRbxkIcDg=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -2401,7 +2442,7 @@ "gzip-size": ["gzip-size@7.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA=="], - "h3": ["h3@1.15.10", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-YzJeWSkDZxAhvmp8dexjRK5hxziRO7I9m0N53WhvYL5NiWfkUkzssVzY9jvGu0HBoLFW6+duYmNSn6MaZBCCtg=="], + "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -2413,7 +2454,7 @@ "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], - "hookable": ["hookable@6.1.0", "", {}, "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw=="], + "hookable": ["hookable@6.1.1", "", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="], "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], @@ -2425,7 +2466,7 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "httpxy": ["httpxy@0.3.1", "", {}, "sha512-XjG/CEoofEisMrnFr0D6U6xOZ4mRfnwcYQ9qvvnT4lvnX8BoeA3x3WofB75D+vZwpaobFVkBIHrZzoK40w8XSw=="], + "httpxy": ["httpxy@0.5.1", "", {}, "sha512-JPhqYiixe1A1I+MXDewWDZqeudBGU8Q9jCHYN8ML+779RQzLjTi78HBvWz4jMxUD6h2/vUL12g4q/mFM0OUw1A=="], "human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="], @@ -2543,6 +2584,30 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -2671,11 +2736,11 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "next": ["next@16.2.2", "", { "dependencies": { "@next/env": "16.2.2", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.2", "@next/swc-darwin-x64": "16.2.2", "@next/swc-linux-arm64-gnu": "16.2.2", "@next/swc-linux-arm64-musl": "16.2.2", "@next/swc-linux-x64-gnu": "16.2.2", "@next/swc-linux-x64-musl": "16.2.2", "@next/swc-win32-arm64-msvc": "16.2.2", "@next/swc-win32-x64-msvc": "16.2.2", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A=="], + "next": ["next@16.2.4", "", { "dependencies": { "@next/env": "16.2.4", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.4", "@next/swc-darwin-x64": "16.2.4", "@next/swc-linux-arm64-gnu": "16.2.4", "@next/swc-linux-arm64-musl": "16.2.4", "@next/swc-linux-x64-gnu": "16.2.4", "@next/swc-linux-x64-musl": "16.2.4", "@next/swc-win32-arm64-msvc": "16.2.4", "@next/swc-win32-x64-msvc": "16.2.4", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q=="], "next_test_app": ["next_test_app@workspace:apps/Next_test_app"], - "nitropack": ["nitropack@2.13.2", "", { "dependencies": { "@cloudflare/kv-asset-handler": "^0.4.2", "@rollup/plugin-alias": "^6.0.0", "@rollup/plugin-commonjs": "^29.0.2", "@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-terser": "^1.0.0", "@vercel/nft": "^1.4.0", "archiver": "^7.0.1", "c12": "^3.3.3", "chokidar": "^5.0.0", "citty": "^0.2.1", "compatx": "^0.2.0", "confbox": "^0.2.4", "consola": "^3.4.2", "cookie-es": "^2.0.0", "croner": "^10.0.1", "crossws": "^0.3.5", "db0": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "dot-prop": "^10.1.0", "esbuild": "^0.27.4", "escape-string-regexp": "^5.0.0", "etag": "^1.8.1", "exsolve": "^1.0.8", "globby": "^16.1.1", "gzip-size": "^7.0.0", "h3": "^1.15.9", "hookable": "^5.5.3", "httpxy": "^0.3.1", "ioredis": "^5.10.1", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "listhen": "^1.9.0", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mime": "^4.1.0", "mlly": "^1.8.2", "node-fetch-native": "^1.6.7", "node-mock-http": "^1.0.4", "ofetch": "^1.5.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", "pretty-bytes": "^7.1.0", "radix3": "^1.1.2", "rollup": "^4.59.0", "rollup-plugin-visualizer": "^7.0.1", "scule": "^1.3.0", "semver": "^7.7.4", "serve-placeholder": "^2.0.2", "serve-static": "^2.2.1", "source-map": "^0.7.6", "std-env": "^4.0.0", "ufo": "^1.6.3", "ultrahtml": "^1.6.0", "uncrypto": "^0.1.3", "unctx": "^2.5.0", "unenv": "^2.0.0-rc.24", "unimport": "^6.0.2", "unplugin-utils": "^0.3.1", "unstorage": "^1.17.4", "untyped": "^2.0.0", "unwasm": "^0.5.3", "youch": "^4.1.0", "youch-core": "^0.3.3" }, "peerDependencies": { "xml2js": "^0.6.2" }, "optionalPeers": ["xml2js"], "bin": { "nitro": "dist/cli/index.mjs", "nitropack": "dist/cli/index.mjs" } }, "sha512-R5TMzSBoTDG4gi6Y+pvvyCNnooShHePHsHxMLP9EXDGdrlR5RvNdSd4e5k8z0/EzP9Ske7ABRMDWg6O7Dm2OYw=="], + "nitropack": ["nitropack@2.13.4", "", { "dependencies": { "@cloudflare/kv-asset-handler": "^0.4.2", "@rollup/plugin-alias": "^6.0.0", "@rollup/plugin-commonjs": "^29.0.2", "@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-terser": "^1.0.0", "@vercel/nft": "^1.5.0", "archiver": "^7.0.1", "c12": "^3.3.4", "chokidar": "^5.0.0", "citty": "^0.2.2", "compatx": "^0.2.0", "confbox": "^0.2.4", "consola": "^3.4.2", "cookie-es": "^2.0.1", "croner": "^10.0.1", "crossws": "^0.3.5", "db0": "^0.3.4", "defu": "^6.1.7", "destr": "^2.0.5", "dot-prop": "^10.1.0", "esbuild": "^0.28.0", "escape-string-regexp": "^5.0.0", "etag": "^1.8.1", "exsolve": "^1.0.8", "globby": "^16.2.0", "gzip-size": "^7.0.0", "h3": "^1.15.11", "hookable": "^5.5.3", "httpxy": "^0.5.1", "ioredis": "^5.10.1", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "listhen": "^1.9.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mime": "^4.1.0", "mlly": "^1.8.2", "node-fetch-native": "^1.6.7", "node-mock-http": "^1.0.4", "ofetch": "^1.5.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.1", "pretty-bytes": "^7.1.0", "radix3": "^1.1.2", "rollup": "^4.60.2", "rollup-plugin-visualizer": "^7.0.1", "scule": "^1.3.0", "semver": "^7.7.4", "serve-placeholder": "^2.0.2", "serve-static": "^2.2.1", "source-map": "^0.7.6", "std-env": "^4.1.0", "ufo": "^1.6.4", "ultrahtml": "^1.6.0", "uncrypto": "^0.1.3", "unctx": "^2.5.0", "unenv": "2.0.0-rc.24", "unimport": "^6.2.0", "unplugin-utils": "^0.3.1", "unstorage": "^1.17.5", "untyped": "^2.0.0", "unwasm": "^0.5.3", "youch": "^4.1.1", "youch-core": "^0.3.3" }, "peerDependencies": { "xml2js": "^0.6.2" }, "optionalPeers": ["xml2js"], "bin": { "nitro": "dist/cli/index.mjs", "nitropack": "dist/cli/index.mjs" } }, "sha512-tX7bT6zxNeMwkc6hxHiZeUoTOjVrcjoh1Z3cmxOlodIqjl4HISgqfGOmkWSayky3Nv9Z5+KQH52F8nmXJY5AAA=="], "node-abi": ["node-abi@3.89.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="], @@ -2707,11 +2772,11 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "nuxt": ["nuxt@4.4.2", "", { "dependencies": { "@dxup/nuxt": "^0.4.0", "@nuxt/cli": "^3.34.0", "@nuxt/devtools": "^3.2.3", "@nuxt/kit": "4.4.2", "@nuxt/nitro-server": "4.4.2", "@nuxt/schema": "4.4.2", "@nuxt/telemetry": "^2.7.0", "@nuxt/vite-builder": "4.4.2", "@unhead/vue": "^2.1.12", "@vue/shared": "^3.5.30", "c12": "^3.3.3", "chokidar": "^5.0.0", "compatx": "^0.2.0", "consola": "^3.4.2", "cookie-es": "^2.0.0", "defu": "^6.1.4", "devalue": "^5.6.3", "errx": "^0.1.0", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "hookable": "^6.0.1", "ignore": "^7.0.5", "impound": "^1.1.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.1", "nanotar": "^0.3.0", "nypm": "^0.6.5", "ofetch": "^1.5.1", "ohash": "^2.0.11", "on-change": "^6.0.2", "oxc-minify": "^0.117.0", "oxc-parser": "^0.117.0", "oxc-transform": "^0.117.0", "oxc-walker": "^0.7.0", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "rou3": "^0.8.1", "scule": "^1.3.0", "semver": "^7.7.4", "std-env": "^4.0.0", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "ultrahtml": "^1.6.0", "uncrypto": "^0.1.3", "unctx": "^2.5.0", "unimport": "^6.0.1", "unplugin": "^3.0.0", "unrouting": "^0.1.5", "untyped": "^2.0.0", "vue": "^3.5.30", "vue-router": "^5.0.3" }, "peerDependencies": { "@parcel/watcher": "^2.1.0", "@types/node": ">=18.12.0" }, "optionalPeers": ["@parcel/watcher", "@types/node"], "bin": { "nuxi": "bin/nuxt.mjs", "nuxt": "bin/nuxt.mjs" } }, "sha512-iWVFpr/YEqVU/CenqIHMnIkvb2HE/9f+q8oxZ+pj2et+60NljGRClCgnmbvGPdmNFE0F1bEhoBCYfqbDOCim3Q=="], + "nuxt": ["nuxt@4.4.4", "", { "dependencies": { "@dxup/nuxt": "^0.4.1", "@nuxt/cli": "^3.35.1", "@nuxt/devtools": "^3.2.4", "@nuxt/kit": "4.4.4", "@nuxt/nitro-server": "4.4.4", "@nuxt/schema": "4.4.4", "@nuxt/telemetry": "^2.8.0", "@nuxt/vite-builder": "4.4.4", "@unhead/vue": "^2.1.13", "@vue/shared": "^3.5.33", "chokidar": "^5.0.0", "compatx": "^0.2.0", "consola": "^3.4.2", "cookie-es": "^2.0.1", "defu": "^6.1.7", "devalue": "^5.7.1", "errx": "^0.1.0", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "hookable": "^6.1.1", "ignore": "^7.0.5", "impound": "^1.1.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.2", "nanotar": "^0.3.0", "nypm": "^0.6.6", "ofetch": "^1.5.1", "ohash": "^2.0.11", "on-change": "^6.0.2", "oxc-minify": "^0.128.0", "oxc-parser": "^0.128.0", "oxc-transform": "^0.128.0", "oxc-walker": "^0.7.0", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "picomatch": "^4.0.4", "pkg-types": "^2.3.1", "rou3": "^0.8.1", "scule": "^1.3.0", "semver": "^7.7.4", "std-env": "^4.1.0", "tinyglobby": "^0.2.16", "ufo": "^1.6.4", "ultrahtml": "^1.6.0", "uncrypto": "^0.1.3", "unctx": "^2.5.0", "unimport": "^6.2.0", "unplugin": "^3.0.0", "unrouting": "^0.1.7", "untyped": "^2.0.0", "vue": "^3.5.33", "vue-router": "^5.0.6" }, "peerDependencies": { "@parcel/watcher": "^2.1.0", "@types/node": ">=18.12.0" }, "optionalPeers": ["@parcel/watcher", "@types/node"], "bin": { "nuxi": "bin/nuxt.mjs", "nuxt": "bin/nuxt.mjs" } }, "sha512-r9E3PYo+uJazltAmjm0TwFW3MQ++Wd//2uRZgCyqkt7VSAVJ5KnRRwUF7JktK/NZbLYAUDiV3tgqE9ZYbHbymA=="], "nuxt_test_app": ["nuxt_test_app@workspace:apps/Nuxt_test_app"], - "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + "nypm": ["nypm@0.6.6", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -2737,11 +2802,11 @@ "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], - "oxc-minify": ["oxc-minify@0.117.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm-eabi": "0.117.0", "@oxc-minify/binding-android-arm64": "0.117.0", "@oxc-minify/binding-darwin-arm64": "0.117.0", "@oxc-minify/binding-darwin-x64": "0.117.0", "@oxc-minify/binding-freebsd-x64": "0.117.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.117.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.117.0", "@oxc-minify/binding-linux-arm64-gnu": "0.117.0", "@oxc-minify/binding-linux-arm64-musl": "0.117.0", "@oxc-minify/binding-linux-ppc64-gnu": "0.117.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.117.0", "@oxc-minify/binding-linux-riscv64-musl": "0.117.0", "@oxc-minify/binding-linux-s390x-gnu": "0.117.0", "@oxc-minify/binding-linux-x64-gnu": "0.117.0", "@oxc-minify/binding-linux-x64-musl": "0.117.0", "@oxc-minify/binding-openharmony-arm64": "0.117.0", "@oxc-minify/binding-wasm32-wasi": "0.117.0", "@oxc-minify/binding-win32-arm64-msvc": "0.117.0", "@oxc-minify/binding-win32-ia32-msvc": "0.117.0", "@oxc-minify/binding-win32-x64-msvc": "0.117.0" } }, "sha512-JHsv/b+bmBJkAzkHXgTN7RThloVxLHPT0ojHfjqxVeHuQB7LPpLUbJ2qfwz37sto9stZ9+AVwUP4b3gtR7p/Tw=="], + "oxc-minify": ["oxc-minify@0.128.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm-eabi": "0.128.0", "@oxc-minify/binding-android-arm64": "0.128.0", "@oxc-minify/binding-darwin-arm64": "0.128.0", "@oxc-minify/binding-darwin-x64": "0.128.0", "@oxc-minify/binding-freebsd-x64": "0.128.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.128.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.128.0", "@oxc-minify/binding-linux-arm64-gnu": "0.128.0", "@oxc-minify/binding-linux-arm64-musl": "0.128.0", "@oxc-minify/binding-linux-ppc64-gnu": "0.128.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.128.0", "@oxc-minify/binding-linux-riscv64-musl": "0.128.0", "@oxc-minify/binding-linux-s390x-gnu": "0.128.0", "@oxc-minify/binding-linux-x64-gnu": "0.128.0", "@oxc-minify/binding-linux-x64-musl": "0.128.0", "@oxc-minify/binding-openharmony-arm64": "0.128.0", "@oxc-minify/binding-wasm32-wasi": "0.128.0", "@oxc-minify/binding-win32-arm64-msvc": "0.128.0", "@oxc-minify/binding-win32-ia32-msvc": "0.128.0", "@oxc-minify/binding-win32-x64-msvc": "0.128.0" } }, "sha512-VIXQO2W886aB+N17yV55Sack6aCpbUqtuNAYhNcPV6dFiWIZ5+kwOjvvw36igWwoljfjWhasu99CQ5wtvPJDYg=="], - "oxc-parser": ["oxc-parser@0.117.0", "", { "dependencies": { "@oxc-project/types": "^0.117.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.117.0", "@oxc-parser/binding-android-arm64": "0.117.0", "@oxc-parser/binding-darwin-arm64": "0.117.0", "@oxc-parser/binding-darwin-x64": "0.117.0", "@oxc-parser/binding-freebsd-x64": "0.117.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.117.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.117.0", "@oxc-parser/binding-linux-arm64-gnu": "0.117.0", "@oxc-parser/binding-linux-arm64-musl": "0.117.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.117.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.117.0", "@oxc-parser/binding-linux-riscv64-musl": "0.117.0", "@oxc-parser/binding-linux-s390x-gnu": "0.117.0", "@oxc-parser/binding-linux-x64-gnu": "0.117.0", "@oxc-parser/binding-linux-x64-musl": "0.117.0", "@oxc-parser/binding-openharmony-arm64": "0.117.0", "@oxc-parser/binding-wasm32-wasi": "0.117.0", "@oxc-parser/binding-win32-arm64-msvc": "0.117.0", "@oxc-parser/binding-win32-ia32-msvc": "0.117.0", "@oxc-parser/binding-win32-x64-msvc": "0.117.0" } }, "sha512-l3cbgK5wUvWDVNWM/JFU77qDdGZK1wudnLsFcrRyNo/bL1CyU8pC25vDhMHikVY29lbK2InTWsX42RxVSutUdQ=="], + "oxc-parser": ["oxc-parser@0.128.0", "", { "dependencies": { "@oxc-project/types": "^0.128.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.128.0", "@oxc-parser/binding-android-arm64": "0.128.0", "@oxc-parser/binding-darwin-arm64": "0.128.0", "@oxc-parser/binding-darwin-x64": "0.128.0", "@oxc-parser/binding-freebsd-x64": "0.128.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.128.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.128.0", "@oxc-parser/binding-linux-arm64-gnu": "0.128.0", "@oxc-parser/binding-linux-arm64-musl": "0.128.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.128.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.128.0", "@oxc-parser/binding-linux-riscv64-musl": "0.128.0", "@oxc-parser/binding-linux-s390x-gnu": "0.128.0", "@oxc-parser/binding-linux-x64-gnu": "0.128.0", "@oxc-parser/binding-linux-x64-musl": "0.128.0", "@oxc-parser/binding-openharmony-arm64": "0.128.0", "@oxc-parser/binding-wasm32-wasi": "0.128.0", "@oxc-parser/binding-win32-arm64-msvc": "0.128.0", "@oxc-parser/binding-win32-ia32-msvc": "0.128.0", "@oxc-parser/binding-win32-x64-msvc": "0.128.0" } }, "sha512-XkOw3eiIxAgQ19WRew/Bq9wc5Ga/guaWIzDBzq80z1PyuDNGvWBpPby9k6YGwV8A8uMw+Nlq3xqlzuDYmUFYUw=="], - "oxc-transform": ["oxc-transform@0.117.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm-eabi": "0.117.0", "@oxc-transform/binding-android-arm64": "0.117.0", "@oxc-transform/binding-darwin-arm64": "0.117.0", "@oxc-transform/binding-darwin-x64": "0.117.0", "@oxc-transform/binding-freebsd-x64": "0.117.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.117.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.117.0", "@oxc-transform/binding-linux-arm64-gnu": "0.117.0", "@oxc-transform/binding-linux-arm64-musl": "0.117.0", "@oxc-transform/binding-linux-ppc64-gnu": "0.117.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.117.0", "@oxc-transform/binding-linux-riscv64-musl": "0.117.0", "@oxc-transform/binding-linux-s390x-gnu": "0.117.0", "@oxc-transform/binding-linux-x64-gnu": "0.117.0", "@oxc-transform/binding-linux-x64-musl": "0.117.0", "@oxc-transform/binding-openharmony-arm64": "0.117.0", "@oxc-transform/binding-wasm32-wasi": "0.117.0", "@oxc-transform/binding-win32-arm64-msvc": "0.117.0", "@oxc-transform/binding-win32-ia32-msvc": "0.117.0", "@oxc-transform/binding-win32-x64-msvc": "0.117.0" } }, "sha512-u1Stl2uhDh9bFuOGjGXQIqx46IRUNMyHQkq59LayXNGS2flNv7RpZpRSWs5S5deuNP6jJZ12gtMBze+m4dOhmw=="], + "oxc-transform": ["oxc-transform@0.128.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm-eabi": "0.128.0", "@oxc-transform/binding-android-arm64": "0.128.0", "@oxc-transform/binding-darwin-arm64": "0.128.0", "@oxc-transform/binding-darwin-x64": "0.128.0", "@oxc-transform/binding-freebsd-x64": "0.128.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.128.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.128.0", "@oxc-transform/binding-linux-arm64-gnu": "0.128.0", "@oxc-transform/binding-linux-arm64-musl": "0.128.0", "@oxc-transform/binding-linux-ppc64-gnu": "0.128.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.128.0", "@oxc-transform/binding-linux-riscv64-musl": "0.128.0", "@oxc-transform/binding-linux-s390x-gnu": "0.128.0", "@oxc-transform/binding-linux-x64-gnu": "0.128.0", "@oxc-transform/binding-linux-x64-musl": "0.128.0", "@oxc-transform/binding-openharmony-arm64": "0.128.0", "@oxc-transform/binding-wasm32-wasi": "0.128.0", "@oxc-transform/binding-win32-arm64-msvc": "0.128.0", "@oxc-transform/binding-win32-ia32-msvc": "0.128.0", "@oxc-transform/binding-win32-x64-msvc": "0.128.0" } }, "sha512-8DfEHlmUiLOHlCK9DGX+d5tORc1xwPPvoRSHSJCYgLHyGjKp4PvfBrvgi59DkEW0SMOWfO8GL9t+R7vdKtupbg=="], "oxc-walker": ["oxc-walker@0.7.0", "", { "dependencies": { "magic-regexp": "^0.10.0" }, "peerDependencies": { "oxc-parser": ">=0.98.0" } }, "sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A=="], @@ -2897,6 +2962,8 @@ "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], @@ -2915,7 +2982,7 @@ "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - "rc9": ["rc9@3.0.0", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.5" } }, "sha512-MGOue0VqscKWQ104udASX/3GYDcKyPI4j4F8gu/jHHzglpmy9a/anZK3PNe8ug6aZFl+9GxLtdhe3kVZuMaQbA=="], + "rc9": ["rc9@3.0.1", "", { "dependencies": { "defu": "^6.1.6", "destr": "^2.0.5" } }, "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ=="], "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], @@ -2951,10 +3018,14 @@ "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="], + "rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="], "rollup-plugin-dts": ["rollup-plugin-dts@6.4.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@jridgewell/sourcemap-codec": "^1.5.5", "convert-source-map": "^2.0.0", "magic-string": "^0.30.21" }, "optionalDependencies": { "@babel/code-frame": "^7.29.0" }, "peerDependencies": { "rollup": "^3.29.4 || ^4", "typescript": "^4.5 || ^5.0 || ^6.0" } }, "sha512-l//F3Zf7ID5GoOfLfD8kroBjQKEKpy1qfhtAdnpibFZMffPaylrg1CoDC2vGkPeTeyxUe4bVFCln2EFuL7IGGg=="], @@ -2985,7 +3056,7 @@ "serialize-javascript": ["serialize-javascript@7.0.5", "", {}, "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw=="], - "seroval": ["seroval@1.5.1", "", {}, "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA=="], + "seroval": ["seroval@1.5.3", "", {}, "sha512-BXe0x4buEeYiIKaRUnth1WqCILQ3k4O67KP/B4pC3pVz0Mv2c96ngA9QDREUYxWY1sb2RZVRqwI9RcpVMyHCVw=="], "serve-placeholder": ["serve-placeholder@2.0.2", "", { "dependencies": { "defu": "^6.1.4" } }, "sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ=="], @@ -3087,7 +3158,7 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "svelte": ["svelte@5.55.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-QjvU7EFemf6mRzdMGlAFttMWtAAVXrax61SZYHdkD6yoVGQ89VeyKfZD4H1JrV1WLmJBxWhFch9H6ig/87VGjw=="], + "svelte": ["svelte@5.55.5", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw=="], "svelte_test_app": ["svelte_test_app@workspace:apps/svelte_test_app"], @@ -3169,7 +3240,7 @@ "typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="], - "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], "ulid": ["ulid@3.0.2", "", { "bin": { "ulid": "dist/cli.js" } }, "sha512-yu26mwteFYzBAot7KVMqFGCVpsF6g8wXfJzQUHvu1no3+rRRSFcSV2nKeYvNPLD2J4b08jYBDhHUjeH0ygIl9w=="], @@ -3185,11 +3256,11 @@ "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], - "unhead": ["unhead@2.1.12", "", { "dependencies": { "hookable": "^6.0.1" } }, "sha512-iTHdWD9ztTunOErtfUFk6Wr11BxvzumcYJ0CzaSCBUOEtg+DUZ9+gnE99i8QkLFT2q1rZD48BYYGXpOZVDLYkA=="], + "unhead": ["unhead@2.1.13", "", { "dependencies": { "hookable": "^6.0.1" } }, "sha512-jO9M1sI6b2h/1KpIu4Jeu+ptumLmUKboRRLxys5pYHFeT+lqTzfNHbYUX9bxVDhC1FBszAGuWcUVlmvIPsah8Q=="], "unicorn-magic": ["unicorn-magic@0.4.0", "", {}, "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw=="], - "unimport": ["unimport@6.0.2", "", { "dependencies": { "acorn": "^8.16.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1" } }, "sha512-ZSOkrDw380w+KIPniY3smyXh2h7H9v2MNr9zejDuh239o5sdea44DRAYrv+rfUi2QGT186P2h0GPGKvy8avQ5g=="], + "unimport": ["unimport@6.2.0", "", { "dependencies": { "acorn": "^8.16.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.2", "pathe": "^2.0.3", "picomatch": "^4.0.4", "pkg-types": "^2.3.1", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.16", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "oxc-parser": "*" }, "optionalPeers": ["oxc-parser"] }, "sha512-4NcqaphAHQff4eBWQ3pjVOCYNLlmVGGMoLDmboobh8+OQe9yP7UyeoMP043M1bG0YNc3CqtukD2VuINxOqm4rQ=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], @@ -3233,7 +3304,7 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + "vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="], "vite-dev-rpc": ["vite-dev-rpc@1.1.0", "", { "dependencies": { "birpc": "^2.4.0", "vite-hot-client": "^2.1.0" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" } }, "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A=="], @@ -3241,7 +3312,7 @@ "vite-node": ["vite-node@2.1.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA=="], - "vite-plugin-checker": ["vite-plugin-checker@0.12.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "chokidar": "^4.0.3", "npm-run-path": "^6.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.3", "tiny-invariant": "^1.3.3", "tinyglobby": "^0.2.15", "vscode-uri": "^3.1.0" }, "peerDependencies": { "@biomejs/biome": ">=1.7", "eslint": ">=9.39.1", "meow": "^13.2.0", "optionator": "^0.9.4", "oxlint": ">=1", "stylelint": ">=16", "typescript": "*", "vite": ">=5.4.21", "vls": "*", "vti": "*", "vue-tsc": "~2.2.10 || ^3.0.0" }, "optionalPeers": ["@biomejs/biome", "eslint", "meow", "optionator", "oxlint", "stylelint", "typescript", "vls", "vti", "vue-tsc"] }, "sha512-CmdZdDOGss7kdQwv73UyVgLPv0FVYe5czAgnmRX2oKljgEvSrODGuClaV3PDR2+3ou7N/OKGauDDBjy2MB07Rg=="], + "vite-plugin-checker": ["vite-plugin-checker@0.13.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "chokidar": "^4.0.3", "npm-run-path": "^6.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.4", "proper-lockfile": "^4.1.2", "tiny-invariant": "^1.3.3", "tinyglobby": "^0.2.15", "vscode-uri": "^3.1.0" }, "peerDependencies": { "@biomejs/biome": ">=1.7", "eslint": ">=9.39.4", "meow": "^13.2.0 || ^14.0.0", "optionator": "^0.9.4", "oxlint": ">=1", "stylelint": ">=16.26.1", "typescript": "*", "vite": ">=5.4.21", "vls": "*", "vti": "*", "vue-tsc": "~2.2.10 || ^3.0.0" }, "optionalPeers": ["@biomejs/biome", "eslint", "meow", "optionator", "oxlint", "stylelint", "typescript", "vls", "vti", "vue-tsc"] }, "sha512-14EkOZmfinVZNxRmg2uCNDwtqGc/33lU/UEJansHgu27+ad+r6mMBf1Xtnq57jGZWiO/xzwtiEKPYsganw7ZFQ=="], "vite-plugin-inspect": ["vite-plugin-inspect@11.3.3", "", { "dependencies": { "ansis": "^4.1.0", "debug": "^4.4.1", "error-stack-parser-es": "^1.0.5", "ohash": "^2.0.11", "open": "^10.2.0", "perfect-debounce": "^2.0.0", "sirv": "^3.0.1", "unplugin-utils": "^0.3.0", "vite-dev-rpc": "^1.1.0" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0-0" } }, "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA=="], @@ -3255,13 +3326,13 @@ "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], - "vue": ["vue@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/compiler-sfc": "3.5.31", "@vue/runtime-dom": "3.5.31", "@vue/server-renderer": "3.5.31", "@vue/shared": "3.5.31" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q=="], + "vue": ["vue@3.5.33", "", { "dependencies": { "@vue/compiler-dom": "3.5.33", "@vue/compiler-sfc": "3.5.33", "@vue/runtime-dom": "3.5.33", "@vue/server-renderer": "3.5.33", "@vue/shared": "3.5.33" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ=="], "vue-bundle-renderer": ["vue-bundle-renderer@2.2.0", "", { "dependencies": { "ufo": "^1.6.1" } }, "sha512-sz/0WEdYH1KfaOm0XaBmRZOWgYTEvUDt6yPYaUzl4E52qzgWLlknaPPTTZmp6benaPTlQAI/hN1x3tAzZygycg=="], "vue-devtools-stub": ["vue-devtools-stub@0.1.0", "", {}, "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ=="], - "vue-router": ["vue-router@5.0.4", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg=="], + "vue-router": ["vue-router@5.0.6", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-9+kmUTGbKMyW9Asoy98IXXYIzrTMT7JDAdpDDeEkorHvybpUvBI2wsrSM5jFOXrFydpzRFJ9vAh+80DN2PGu9w=="], "vue-sfc-transformer": ["vue-sfc-transformer@0.1.17", "", { "dependencies": { "@babel/parser": "^7.27.0" }, "peerDependencies": { "@vue/compiler-core": "^3.5.13", "esbuild": "*", "vue": "^3.5.13" } }, "sha512-0mpkDTWm1ybtp/Mp3vhrXP4r8yxcGF+quxGyJfrHDl2tl5naQjK3xkIGaVR5BtR5KG1LWJbdCrqn7I6f460j9A=="], @@ -3325,6 +3396,8 @@ "@dxup/nuxt/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "@dxup/nuxt/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -3343,62 +3416,104 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "@nuxt/cli/c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="], + "@nuxt/cli/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + "@nuxt/cli/nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + "@nuxt/cli/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "@nuxt/cli/tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], + "@nuxt/cli/ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + + "@nuxt/devtools/@nuxt/kit": ["@nuxt/kit@4.4.2", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "mlly": "^1.8.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-5+IPRNX2CjkBhuWUwz0hBuLqiaJPRoKzQ+SvcdrQDbAyE+VDeFt74VpSFr5/R0ujrK4b+XnSHUJWdS72w6hsog=="], + + "@nuxt/devtools/hookable": ["hookable@6.1.0", "", {}, "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw=="], + "@nuxt/devtools/magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], - "@nuxt/devtools/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "@nuxt/devtools/nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], - "@nuxt/devtools/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + "@nuxt/devtools/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "@nuxt/devtools/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], - "@nuxt/devtools-kit/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + "@nuxt/devtools-kit/@nuxt/kit": ["@nuxt/kit@4.4.2", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "mlly": "^1.8.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-5+IPRNX2CjkBhuWUwz0hBuLqiaJPRoKzQ+SvcdrQDbAyE+VDeFt74VpSFr5/R0ujrK4b+XnSHUJWdS72w6hsog=="], "@nuxt/devtools-wizard/magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], "@nuxt/devtools-wizard/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "@nuxt/kit/defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + "@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@nuxt/kit/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "@nuxt/kit/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + + "@nuxt/kit/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "@nuxt/module-builder/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "@nuxt/nitro-server/defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + "@nuxt/nitro-server/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "@nuxt/nitro-server/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "@nuxt/nitro-server/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + "@nuxt/nitro-server/std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + + "@nuxt/schema/defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], "@nuxt/schema/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "@nuxt/schema/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + "@nuxt/schema/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], - "@nuxt/telemetry/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + "@nuxt/schema/std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + + "@nuxt/telemetry/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], "@nuxt/telemetry/ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="], - "@nuxt/vite-builder/@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.5", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg=="], + "@nuxt/telemetry/std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + + "@nuxt/vite-builder/@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.6", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.13" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg=="], + + "@nuxt/vite-builder/autoprefixer": ["autoprefixer@10.5.0", "", { "dependencies": { "browserslist": "^4.28.2", "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong=="], + + "@nuxt/vite-builder/cssnano": ["cssnano@7.1.9", "", { "dependencies": { "cssnano-preset-default": "^7.0.17", "lilconfig": "^3.1.3" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-uPR75+5Dk/WJ/YSPR1/YDHdwMM9c5FsaARljfKWgeCKLKOtJ0we21xy/RcCjn53fZnD/f6yYEIZ8pu18+GnbNQ=="], + + "@nuxt/vite-builder/defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], "@nuxt/vite-builder/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "@nuxt/vite-builder/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "@nuxt/vite-builder/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + "@nuxt/vite-builder/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + + "@nuxt/vite-builder/postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + + "@nuxt/vite-builder/std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], - "@nuxt/vite-builder/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + "@nuxt/vite-builder/vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], "@nuxt/vite-builder/vite-node": ["vite-node@5.3.0", "", { "dependencies": { "cac": "^6.7.14", "es-module-lexer": "^2.0.0", "obug": "^2.1.1", "pathe": "^2.0.3", "vite": "^7.3.1" }, "bin": { "vite-node": "dist/cli.mjs" } }, "sha512-8f20COPYJujc3OKPX6OuyBy3ZIv2det4eRRU4GY1y2MjbeGSUmPjedxg1b72KnTagCofwvZ65ThzjxDW2AtQFQ=="], + "@oxc-minify/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@oxc-parser/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@oxc-transform/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="], "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], @@ -3419,16 +3534,46 @@ "@vercel/nft/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "@vitejs/plugin-vue/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "@vitejs/plugin-vue-jsx/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="], + + "@vitest/mocker/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "@vue-macros/common/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.31", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.31", "@vue/compiler-dom": "3.5.31", "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q=="], + + "@vue/babel-plugin-jsx/@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="], + + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.31", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.31", "@vue/compiler-dom": "3.5.31", "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q=="], + + "@vue/compiler-core/@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="], + "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.33", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/shared": "3.5.33", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw=="], + + "@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.33", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/shared": "3.5.33", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw=="], + "@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@vue/compiler-sfc/postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + "@vue/devtools-kit/birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], "@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + "@vue/language-core/@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="], + + "@vue/language-core/@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="], + "@vue/language-core/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "@vueuse/core/vue": ["vue@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/compiler-sfc": "3.5.31", "@vue/runtime-dom": "3.5.31", "@vue/server-renderer": "3.5.31", "@vue/shared": "3.5.31" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q=="], + + "@vueuse/integrations/vue": ["vue@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/compiler-sfc": "3.5.31", "@vue/runtime-dom": "3.5.31", "@vue/server-renderer": "3.5.31", "@vue/shared": "3.5.31" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q=="], + + "@vueuse/shared/vue": ["vue@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/compiler-sfc": "3.5.31", "@vue/runtime-dom": "3.5.31", "@vue/server-renderer": "3.5.31", "@vue/shared": "3.5.31" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q=="], + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "archiver/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], @@ -3441,6 +3586,8 @@ "ast-kit/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], + "blog-next/@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "blog-next/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], @@ -3451,15 +3598,21 @@ "blog-sveltekit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="], + + "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], + "bullmq/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "c12/giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "c12/defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], "c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "c12/rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + "c12/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + + "caniuse-api/caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], "cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], @@ -3481,7 +3634,9 @@ "global-directory/ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], - "h3/cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], + "h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], + + "h3/defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -3491,6 +3646,12 @@ "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "listhen/h3": ["h3@1.15.10", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-YzJeWSkDZxAhvmp8dexjRK5hxziRO7I9m0N53WhvYL5NiWfkUkzssVzY9jvGu0HBoLFW6+duYmNSn6MaZBCCtg=="], + + "listhen/ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + + "magic-regexp/ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "magic-regexp/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], @@ -3505,6 +3666,8 @@ "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "mlly/ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "next_test_app/@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -3515,7 +3678,11 @@ "nitropack/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "nitropack/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + "nitropack/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], + + "nitropack/defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + + "nitropack/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], "nitropack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -3523,36 +3690,58 @@ "nitropack/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + "nitropack/listhen": ["listhen@1.10.0", "", { "dependencies": { "@parcel/watcher": "^2.5.6", "@parcel/watcher-wasm": "^2.5.6", "citty": "^0.2.2", "consola": "^3.4.2", "crossws": ">=0.2.0 <0.5.0", "defu": "^6.1.7", "get-port-please": "^3.2.0", "h3": "^1.15.11", "http-shutdown": "^1.2.2", "jiti": "^2.6.1", "mlly": "^1.8.2", "node-forge": "^1.4.0", "pathe": "^2.0.3", "std-env": "^4.1.0", "tinyclip": "^0.1.12", "ufo": "^1.6.4", "untun": "^0.1.3", "uqr": "^0.1.3" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-kfz4C0OrC6IpaVMtYDJtf6PFjurxe9NBBoDAh/o2p587INryFOO4DQ9OetbCdDrWFt1m1CJKvYrzkGsuPHw8nQ=="], + "nitropack/magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], "nitropack/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "nitropack/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + "nitropack/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + + "nitropack/rollup": ["rollup@4.60.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.3", "@rollup/rollup-android-arm64": "4.60.3", "@rollup/rollup-darwin-arm64": "4.60.3", "@rollup/rollup-darwin-x64": "4.60.3", "@rollup/rollup-freebsd-arm64": "4.60.3", "@rollup/rollup-freebsd-x64": "4.60.3", "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", "@rollup/rollup-linux-arm-musleabihf": "4.60.3", "@rollup/rollup-linux-arm64-gnu": "4.60.3", "@rollup/rollup-linux-arm64-musl": "4.60.3", "@rollup/rollup-linux-loong64-gnu": "4.60.3", "@rollup/rollup-linux-loong64-musl": "4.60.3", "@rollup/rollup-linux-ppc64-gnu": "4.60.3", "@rollup/rollup-linux-ppc64-musl": "4.60.3", "@rollup/rollup-linux-riscv64-gnu": "4.60.3", "@rollup/rollup-linux-riscv64-musl": "4.60.3", "@rollup/rollup-linux-s390x-gnu": "4.60.3", "@rollup/rollup-linux-x64-gnu": "4.60.3", "@rollup/rollup-linux-x64-musl": "4.60.3", "@rollup/rollup-openbsd-x64": "4.60.3", "@rollup/rollup-openharmony-arm64": "4.60.3", "@rollup/rollup-win32-arm64-msvc": "4.60.3", "@rollup/rollup-win32-ia32-msvc": "4.60.3", "@rollup/rollup-win32-x64-gnu": "4.60.3", "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A=="], + + "nitropack/std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + + "nitropack/youch": ["youch@4.1.1", "", { "dependencies": { "@poppinss/colors": "^4.1.6", "@poppinss/dumper": "^0.7.0", "@speed-highlight/core": "^1.2.14", "cookie-es": "^3.0.1", "youch-core": "^0.3.3" } }, "sha512-mxW3qiSnl+GRxXsaUMzv2Mbada1Y8CDltET9UxejDQe6DBYlSekghl5U5K0ReAikcHDi0G1vKZEmmo/NWAGKLA=="], "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "nuxt/@nuxt/cli": ["@nuxt/cli@3.35.1", "", { "dependencies": { "@bomb.sh/tab": "^0.0.14", "@clack/prompts": "^1.2.0", "c12": "^3.3.4", "citty": "^0.2.2", "confbox": "^0.2.4", "consola": "^3.4.2", "debug": "^4.4.3", "defu": "^6.1.7", "exsolve": "^1.0.8", "fuse.js": "^7.3.0", "fzf": "^0.5.2", "giget": "^3.2.0", "jiti": "^2.6.1", "listhen": "^1.9.1", "nypm": "^0.6.6", "ofetch": "^1.5.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", "scule": "^1.3.0", "semver": "^7.7.4", "srvx": "^0.11.15", "std-env": "^4.1.0", "tinyclip": "^0.1.12", "tinyexec": "^1.1.1", "ufo": "^1.6.3", "youch": "^4.1.1" }, "peerDependencies": { "@nuxt/schema": "^4.4.2" }, "optionalPeers": ["@nuxt/schema"], "bin": { "nuxi": "bin/nuxi.mjs", "nuxi-ng": "bin/nuxi.mjs", "nuxt": "bin/nuxi.mjs", "nuxt-cli": "bin/nuxi.mjs" } }, "sha512-nX9XO+e3l9pnhHL2zsbnBmQb/nsOQYhGz2XiqE8X962QN9ufc1ZSuDZoTmQVv/ymkbYNR6hpNWW8RZQhuhzadw=="], + "nuxt/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + "nuxt/defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + "nuxt/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "nuxt/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "nuxt/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "nuxt/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + "nuxt/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], - "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + "nuxt/std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + + "nuxt/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], "nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "nypm/tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], + "nypm/tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], + + "ofetch/ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "rc9/defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + "react-dom/react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], "react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -3561,6 +3750,8 @@ "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + "rolldown/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + "rollup-plugin-visualizer/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -3597,20 +3788,40 @@ "unimport/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "unimport/pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], + + "unimport/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "unplugin-utils/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "unrouting/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "unrouting/ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + "unstorage/h3": ["h3@1.15.10", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-YzJeWSkDZxAhvmp8dexjRK5hxziRO7I9m0N53WhvYL5NiWfkUkzssVzY9jvGu0HBoLFW6+duYmNSn6MaZBCCtg=="], + + "unstorage/ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "unwasm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "vite/postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + + "vite/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "vite-dev-rpc/birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], + "vite-dev-rpc/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vite-hot-client/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vite-node/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + "vite-plugin-checker/npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + "vite-plugin-checker/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "vite-plugin-inspect/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "vite-plugin-vue-tracer/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -3619,10 +3830,22 @@ "vitepress/@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="], + "vitepress/@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="], + + "vitepress/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vitepress/vue": ["vue@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/compiler-sfc": "3.5.31", "@vue/runtime-dom": "3.5.31", "@vue/server-renderer": "3.5.31", "@vue/shared": "3.5.31" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q=="], + + "vitest/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vue-bundle-renderer/ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "vue-router/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "vue-router/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "vue-router/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -3631,6 +3854,8 @@ "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "youch/cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], + "zip-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], "@dxup/nuxt/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], @@ -3641,18 +3866,108 @@ "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "@nuxt/cli/c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + + "@nuxt/cli/c12/giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + + "@nuxt/cli/c12/rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + + "@nuxt/devtools-kit/@nuxt/kit/c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="], + + "@nuxt/devtools-kit/@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@nuxt/devtools-kit/@nuxt/kit/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "@nuxt/devtools-kit/@nuxt/kit/rc9": ["rc9@3.0.0", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.5" } }, "sha512-MGOue0VqscKWQ104udASX/3GYDcKyPI4j4F8gu/jHHzglpmy9a/anZK3PNe8ug6aZFl+9GxLtdhe3kVZuMaQbA=="], + + "@nuxt/devtools-kit/@nuxt/kit/ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + + "@nuxt/devtools/@nuxt/kit/c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="], + + "@nuxt/devtools/@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@nuxt/devtools/@nuxt/kit/rc9": ["rc9@3.0.0", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.5" } }, "sha512-MGOue0VqscKWQ104udASX/3GYDcKyPI4j4F8gu/jHHzglpmy9a/anZK3PNe8ug6aZFl+9GxLtdhe3kVZuMaQbA=="], + + "@nuxt/devtools/@nuxt/kit/ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + + "@nuxt/devtools/nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + + "@nuxt/devtools/nypm/tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], + "@nuxt/devtools/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], + "@nuxt/vite-builder/@vitejs/plugin-vue/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.13", "", {}, "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA=="], + + "@nuxt/vite-builder/autoprefixer/browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default": ["cssnano-preset-default@7.0.17", "", { "dependencies": { "browserslist": "^4.28.2", "css-declaration-sorter": "^7.2.0", "cssnano-utils": "^5.0.3", "postcss-calc": "^10.1.1", "postcss-colormin": "^7.0.10", "postcss-convert-values": "^7.0.12", "postcss-discard-comments": "^7.0.8", "postcss-discard-duplicates": "^7.0.4", "postcss-discard-empty": "^7.0.3", "postcss-discard-overridden": "^7.0.3", "postcss-merge-longhand": "^7.0.7", "postcss-merge-rules": "^7.0.11", "postcss-minify-font-values": "^7.0.3", "postcss-minify-gradients": "^7.0.5", "postcss-minify-params": "^7.0.9", "postcss-minify-selectors": "^7.1.2", "postcss-normalize-charset": "^7.0.3", "postcss-normalize-display-values": "^7.0.3", "postcss-normalize-positions": "^7.0.4", "postcss-normalize-repeat-style": "^7.0.4", "postcss-normalize-string": "^7.0.3", "postcss-normalize-timing-functions": "^7.0.3", "postcss-normalize-unicode": "^7.0.9", "postcss-normalize-url": "^7.0.3", "postcss-normalize-whitespace": "^7.0.3", "postcss-ordered-values": "^7.0.4", "postcss-reduce-initial": "^7.0.9", "postcss-reduce-transforms": "^7.0.3", "postcss-svgo": "^7.1.3", "postcss-unique-selectors": "^7.0.7" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-11qO63A+czwguQFJCaTdICvbaxn0pJzz/XghLlv+OT7WyToDxAMR0Xb3/26/l0y0hQJywwNbj/SLSQlGBHE1OA=="], + + "@nuxt/vite-builder/vite/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "@nuxt/vite-builder/vite-node/es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + "@nuxt/vite-builder/vite-node/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], "@vercel/nft/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "@vercel/nft/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "@vitejs/plugin-vue/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "@vitest/mocker/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "@vue-macros/common/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="], + + "@vue-macros/common/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], + + "@vue-macros/common/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="], + + "@vue-macros/common/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="], + + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], + + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="], + + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@vue/language-core/minimatch/brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="], + "@vueuse/core/vue/@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="], + + "@vueuse/core/vue/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.31", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.31", "@vue/compiler-dom": "3.5.31", "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q=="], + + "@vueuse/core/vue/@vue/runtime-dom": ["@vue/runtime-dom@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/runtime-core": "3.5.31", "@vue/shared": "3.5.31", "csstype": "^3.2.3" } }, "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g=="], + + "@vueuse/core/vue/@vue/server-renderer": ["@vue/server-renderer@3.5.31", "", { "dependencies": { "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31" }, "peerDependencies": { "vue": "3.5.31" } }, "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA=="], + + "@vueuse/core/vue/@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="], + + "@vueuse/integrations/vue/@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="], + + "@vueuse/integrations/vue/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.31", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.31", "@vue/compiler-dom": "3.5.31", "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q=="], + + "@vueuse/integrations/vue/@vue/runtime-dom": ["@vue/runtime-dom@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/runtime-core": "3.5.31", "@vue/shared": "3.5.31", "csstype": "^3.2.3" } }, "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g=="], + + "@vueuse/integrations/vue/@vue/server-renderer": ["@vue/server-renderer@3.5.31", "", { "dependencies": { "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31" }, "peerDependencies": { "vue": "3.5.31" } }, "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA=="], + + "@vueuse/integrations/vue/@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="], + + "@vueuse/shared/vue/@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="], + + "@vueuse/shared/vue/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.31", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.31", "@vue/compiler-dom": "3.5.31", "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q=="], + + "@vueuse/shared/vue/@vue/runtime-dom": ["@vue/runtime-dom@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/runtime-core": "3.5.31", "@vue/shared": "3.5.31", "csstype": "^3.2.3" } }, "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g=="], + + "@vueuse/shared/vue/@vue/server-renderer": ["@vue/server-renderer@3.5.31", "", { "dependencies": { "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31" }, "peerDependencies": { "vue": "3.5.31" } }, "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA=="], + + "@vueuse/shared/vue/@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="], + "archiver-utils/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "archiver/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], @@ -3831,6 +4146,8 @@ "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "listhen/h3/cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], + "mkdist/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "mkdist/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -3887,10 +4204,130 @@ "nitropack/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "nitropack/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], + + "nitropack/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], + + "nitropack/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="], + + "nitropack/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="], + + "nitropack/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="], + + "nitropack/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="], + + "nitropack/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="], + + "nitropack/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="], + + "nitropack/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="], + + "nitropack/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="], + + "nitropack/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="], + + "nitropack/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="], + + "nitropack/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="], + + "nitropack/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="], + + "nitropack/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="], + + "nitropack/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="], + + "nitropack/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="], + + "nitropack/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="], + + "nitropack/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="], + + "nitropack/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="], + + "nitropack/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="], + + "nitropack/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="], + + "nitropack/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="], + + "nitropack/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="], + + "nitropack/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="], + + "nitropack/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], + "nitropack/globby/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "nitropack/globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + "nitropack/listhen/uqr": ["uqr@0.1.3", "", {}, "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA=="], + + "nitropack/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.3", "", { "os": "android", "cpu": "arm" }, "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw=="], + + "nitropack/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.3", "", { "os": "android", "cpu": "arm64" }, "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw=="], + + "nitropack/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g=="], + + "nitropack/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw=="], + + "nitropack/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ=="], + + "nitropack/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA=="], + + "nitropack/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g=="], + + "nitropack/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w=="], + + "nitropack/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA=="], + + "nitropack/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg=="], + + "nitropack/rollup/@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA=="], + + "nitropack/rollup/@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg=="], + + "nitropack/rollup/@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ=="], + + "nitropack/rollup/@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA=="], + + "nitropack/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw=="], + + "nitropack/rollup/@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ=="], + + "nitropack/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig=="], + + "nitropack/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA=="], + + "nitropack/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA=="], + + "nitropack/rollup/@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q=="], + + "nitropack/rollup/@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.3", "", { "os": "none", "cpu": "arm64" }, "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg=="], + + "nitropack/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg=="], + + "nitropack/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA=="], + + "nitropack/rollup/@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A=="], + + "nitropack/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA=="], + + "nitropack/youch/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + + "nuxt/@nuxt/cli/@clack/prompts": ["@clack/prompts@1.3.0", "", { "dependencies": { "@clack/core": "1.3.0", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw=="], + + "nuxt/@nuxt/cli/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], + + "nuxt/@nuxt/cli/fuse.js": ["fuse.js@7.3.0", "", {}, "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w=="], + + "nuxt/@nuxt/cli/listhen": ["listhen@1.10.0", "", { "dependencies": { "@parcel/watcher": "^2.5.6", "@parcel/watcher-wasm": "^2.5.6", "citty": "^0.2.2", "consola": "^3.4.2", "crossws": ">=0.2.0 <0.5.0", "defu": "^6.1.7", "get-port-please": "^3.2.0", "h3": "^1.15.11", "http-shutdown": "^1.2.2", "jiti": "^2.6.1", "mlly": "^1.8.2", "node-forge": "^1.4.0", "pathe": "^2.0.3", "std-env": "^4.1.0", "tinyclip": "^0.1.12", "ufo": "^1.6.4", "untun": "^0.1.3", "uqr": "^0.1.3" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-kfz4C0OrC6IpaVMtYDJtf6PFjurxe9NBBoDAh/o2p587INryFOO4DQ9OetbCdDrWFt1m1CJKvYrzkGsuPHw8nQ=="], + + "nuxt/@nuxt/cli/srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="], + + "nuxt/@nuxt/cli/tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], + + "nuxt/@nuxt/cli/youch": ["youch@4.1.1", "", { "dependencies": { "@poppinss/colors": "^4.1.6", "@poppinss/dumper": "^0.7.0", "@speed-highlight/core": "^1.2.14", "cookie-es": "^3.0.1", "youch-core": "^0.3.3" } }, "sha512-mxW3qiSnl+GRxXsaUMzv2Mbada1Y8CDltET9UxejDQe6DBYlSekghl5U5K0ReAikcHDi0G1vKZEmmo/NWAGKLA=="], + "nuxt/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], @@ -3961,76 +4398,390 @@ "unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "unstorage/h3/cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], + + "vite-dev-rpc/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "vite-hot-client/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "vite-node/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "vite-plugin-checker/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "vite-plugin-checker/npm-run-path/unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + "vitepress/@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="], - "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + "vitepress/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], - "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + "vitepress/vue/@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="], - "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + "vitepress/vue/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.31", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.31", "@vue/compiler-dom": "3.5.31", "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q=="], - "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + "vitepress/vue/@vue/runtime-dom": ["@vue/runtime-dom@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/runtime-core": "3.5.31", "@vue/shared": "3.5.31", "csstype": "^3.2.3" } }, "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g=="], - "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + "vitepress/vue/@vue/server-renderer": ["@vue/server-renderer@3.5.31", "", { "dependencies": { "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31" }, "peerDependencies": { "vue": "3.5.31" } }, "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA=="], - "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + "vitest/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], - "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + "vue-router/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + "zip-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + "@nuxt/cli/c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + "@nuxt/cli/c12/giget/citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], - "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + "@nuxt/devtools-kit/@nuxt/kit/c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + "@nuxt/devtools-kit/@nuxt/kit/c12/giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], - "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + "@nuxt/devtools-kit/@nuxt/kit/c12/rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], - "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + "@nuxt/devtools/@nuxt/kit/c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + "@nuxt/devtools/@nuxt/kit/c12/giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], - "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + "@nuxt/devtools/@nuxt/kit/c12/rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], - "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + "@nuxt/vite-builder/cssnano/cssnano-preset-default/browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + "@nuxt/vite-builder/cssnano/cssnano-preset-default/cssnano-utils": ["cssnano-utils@5.0.3", "", { "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-ynIREMICLxkxm7e9bCR9sh75s4Q5drICi0ua1yxo5jH2XPBqSKkl4dOh4EbFqtUmnTMhRffHgYL0EKKkMjtJTg=="], - "vitepress/@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="], + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-colormin": ["postcss-colormin@7.0.10", "", { "dependencies": { "@colordx/core": "^5.4.3", "browserslist": "^4.28.2", "caniuse-api": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-yFr6JezOolHLta/buLE71VKPh2mXursp4saVe98/ol8ZnEWhL+racShqPKlvd/DKWLre/39B6HhcMXf7RZ3hxg=="], - "vue-router/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-convert-values": ["postcss-convert-values@7.0.12", "", { "dependencies": { "browserslist": "^4.28.2", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-xurKu5qqk4viR3Cp3p4xBR4KfnZm4w4ys6+UBwBmeuBSNkH7+DtLnYOYnOffgtE4yx8sH9S1VZ6RAAvROXzP2Q=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-discard-comments": ["postcss-discard-comments@7.0.8", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-CvvS5S9WrXblFXCEJ9nVo+4z+eA7zSC7Z88V1HEJuwlQhlFnYTIjg1xJY+BCUiG2bvICap2tXii4mP22BD108Q=="], - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-discard-duplicates": ["postcss-discard-duplicates@7.0.4", "", { "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-VBNn1+EuMZkeGVVtz0gRfbNGtx9IFgAsAV+E2pHtXPrp4qfGBkhTIiAuE/wrb+Y6Pakg9NewAlfTpYIFAWODtw=="], - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-discard-empty": ["postcss-discard-empty@7.0.3", "", { "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-M2pyjQCU+/7cMHVtL6bKTHjv0lZnPLMpicgr67Dlth7AbuV9gjVTtUqaRwn6Pp6BwSDspUzhz8SaUrRykJU5Dw=="], - "zip-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-discard-overridden": ["postcss-discard-overridden@7.0.3", "", { "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-aNovXo9UsZuRNLzHJtp13lHIvinDPfiXBPePpXkSjCbgp++iU2FqE+YxvjIsg6EdyPZsASFbfu+JcBFVsErXIQ=="], - "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-merge-longhand": ["postcss-merge-longhand@7.0.7", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "stylehacks": "^7.0.11" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-b3mfYUxR388u5Pt0HPcVIUtUDn/k15UfTY9M+ORW+meCR6JLNxoZffiYvXyOYQoRYQNZyX/UFkMCM/mNHxe1qA=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-merge-rules": ["postcss-merge-rules@7.0.11", "", { "dependencies": { "browserslist": "^4.28.2", "caniuse-api": "^3.0.0", "cssnano-utils": "^5.0.3", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-SJUPM18g2BmPhf8BVlbwqWz4aK3pLu6u6xjfwEzra7xL6IBR10sUaiB++EzqcVfadPHrKBSMlNdP+XieykhI+Q=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-minify-font-values": ["postcss-minify-font-values@7.0.3", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-yilG/VOaNI74IylQvAQQxm3/wZVBkXyYUqNUAdxqwtbWUXPsbK1q8Ms0mL83v+f8YicgcyfYCRZtWACUdYajpA=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-minify-gradients": ["postcss-minify-gradients@7.0.5", "", { "dependencies": { "@colordx/core": "^5.4.3", "cssnano-utils": "^5.0.3", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-YraROyQRg3BI1+Hg8E05B/JPdnTm8EDSVu4P2BxdM+CRiOyfmou809+chGIqo6fQqwjPGQ947nbGncSjmTU1WQ=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-minify-params": ["postcss-minify-params@7.0.9", "", { "dependencies": { "browserslist": "^4.28.2", "cssnano-utils": "^5.0.3", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-R8itbB8BhlpoYyBm1ou0dD+vJnQ3F6adQipR4UnkCHUwlo+S9WXJaDRg1RHjC8YVAtIdrQzSWvJl40HnGDTKjA=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-minify-selectors": ["postcss-minify-selectors@7.1.2", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-api": "^3.0.0", "cssesc": "^3.0.0", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-aQtrEWKwqafNlExcKHQvPGsXR2+vlUqqJtf5XsCQcgsSb5PL4wlujWBYDJuWsP4UnQX1YHDHU8qRlD+1PzTQ+Q=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-normalize-charset": ["postcss-normalize-charset@7.0.3", "", { "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-NoBfZu8PR4c2NlmjvrqQTzCzLY79hwcSRgNQ3ZiNK0ABzf9kYKloE/jNj+/8GQY1wsm8pRRgANk6ydLH8cwo0Q=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-normalize-display-values": ["postcss-normalize-display-values@7.0.3", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-ldsCX0QIt05pKIOobZtVQ48wXJecr+czw4+e1/YjVhLMqslShgpVxgPtI2CefURR8oyVoYaU/l829MMwExDMLw=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-normalize-positions": ["postcss-normalize-positions@7.0.4", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-VEvlpeGd3Ju1Hqa/oN4jaP3+ms4laYwkEL9N9u+B6k54PZjXbW1n6wI+aVprf1BQXlCYpS5+1pl/7/vHiKgARg=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-normalize-repeat-style": ["postcss-normalize-repeat-style@7.0.4", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-6mPKlY/8cSaDHxX502wERADarJsccwlky6yIrOapHH2ZgfoKAV94SbiTKfKEs4EEpdazuc3J72WsqeYk7hp9+Q=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-normalize-string": ["postcss-normalize-string@7.0.3", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-HnEQPUchi1eznmDKEYrKUTqrprEq97SrpUYClgUkv7V2zRODD9DFoUsYU+m9ZOetmD5ku7fEMZB/lwy8IT6xVQ=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-normalize-timing-functions": ["postcss-normalize-timing-functions@7.0.3", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-zmEzHdvpZBZu0OKlbJSfgASQvaayyAoVuWtvyr34IJ/LyS+DaOKvvR3EvFJ9RWWtNIx+CMvO125OVophaxNYew=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-normalize-unicode": ["postcss-normalize-unicode@7.0.9", "", { "dependencies": { "browserslist": "^4.28.2", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-DRAdWfeh/TjmhLJsw91vdiWCnUod9iwvM7xyS02/nF/sLsCR3A8l3pztrSUrWG8DSBqfX7yEk9FM0USaVJ2mSg=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-normalize-url": ["postcss-normalize-url@7.0.3", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-CL93wmloq5qsffmFv+bw24MIRbmhHrp53qoh1LDAb/5TtjWEXI/np4xcP/Gw9oWCb2XyWnqHYLDUwiKRoJBA1Q=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-normalize-whitespace": ["postcss-normalize-whitespace@7.0.3", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-FdHjjn+Ht5Z2ZRjNOmeCbNq6lq09sUYKpmlF/Aq0XjVNSLTL6fmHlA/3swN2wP2caY9GV/tjSDcIIyS7aN7W0A=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-ordered-values": ["postcss-ordered-values@7.0.4", "", { "dependencies": { "cssnano-utils": "^5.0.3", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-nubSi49hDHQk4E8KIj+IbLY8Bg+8OcSUEhgyolgM+atnOvXjV7EjaR6bac4YGZoFyPa9mWoAF3EaYbWdFkKqVg=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-reduce-initial": ["postcss-reduce-initial@7.0.9", "", { "dependencies": { "browserslist": "^4.28.2", "caniuse-api": "^3.0.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-ztTNPdIxXTxtBcG03E9u8v44M4ElXbMIRT7pf2onlquGula0Y83nKKxqM22FA/hMgkfCjN7ohevkVlaNwI8iOQ=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-reduce-transforms": ["postcss-reduce-transforms@7.0.3", "", { "dependencies": { "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-FXsnN9ZwcZTT8Yf8cAHA8qIGUXcX6WfLd9JoYhrdDfmvsVhhfqkkv7m4AC3rwFOfz+GzkUa87OCKF9dUcicd+g=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-svgo": ["postcss-svgo@7.1.3", "", { "dependencies": { "postcss-value-parser": "^4.2.0", "svgo": "^4.0.1" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-2QfoFOYMcj8lwcVEf9WeTlkVIAm7u2QvOEhMzkQU3KUhhGX/l8hVV9EtjMv4iq3E9iI3OeeMN0YoMLbGusuigw=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-unique-selectors": ["postcss-unique-selectors@7.0.7", "", { "dependencies": { "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-d+sCkaRnSefghOUdH8CMJZV9yUQhj2ojpe8Nw/lA+LV1UOfeleGkLTl6XdCFFSai9UJ+DJPb69FFuqthXYsY8w=="], + + "@nuxt/vite-builder/vite-node/vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "@vercel/nft/glob/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "@vitejs/plugin-vue/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@vitejs/plugin-vue/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@vitest/mocker/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@vitest/mocker/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@vitest/mocker/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@vitest/mocker/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@vitest/mocker/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@vitest/mocker/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@vitest/mocker/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@vitest/mocker/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@vitest/mocker/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@vitest/mocker/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@vitest/mocker/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@vitest/mocker/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@vitest/mocker/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@vitest/mocker/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@vitest/mocker/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@vitest/mocker/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@vitest/mocker/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@vitest/mocker/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@vitest/mocker/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@vitest/mocker/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@vitest/mocker/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@vitest/mocker/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@vitest/mocker/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@vueuse/core/vue/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], + + "@vueuse/core/vue/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vueuse/core/vue/@vue/runtime-dom/@vue/reactivity": ["@vue/reactivity@3.5.31", "", { "dependencies": { "@vue/shared": "3.5.31" } }, "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g=="], + + "@vueuse/core/vue/@vue/runtime-dom/@vue/runtime-core": ["@vue/runtime-core@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q=="], + + "@vueuse/core/vue/@vue/server-renderer/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], + + "@vueuse/integrations/vue/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], + + "@vueuse/integrations/vue/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vueuse/integrations/vue/@vue/runtime-dom/@vue/reactivity": ["@vue/reactivity@3.5.31", "", { "dependencies": { "@vue/shared": "3.5.31" } }, "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g=="], + + "@vueuse/integrations/vue/@vue/runtime-dom/@vue/runtime-core": ["@vue/runtime-core@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q=="], + + "@vueuse/integrations/vue/@vue/server-renderer/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], + + "@vueuse/shared/vue/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], + + "@vueuse/shared/vue/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vueuse/shared/vue/@vue/runtime-dom/@vue/reactivity": ["@vue/reactivity@3.5.31", "", { "dependencies": { "@vue/shared": "3.5.31" } }, "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g=="], + + "@vueuse/shared/vue/@vue/runtime-dom/@vue/runtime-core": ["@vue/runtime-core@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q=="], + + "@vueuse/shared/vue/@vue/server-renderer/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], + + "nuxt/@nuxt/cli/@clack/prompts/@clack/core": ["@clack/core@1.3.0", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA=="], + + "nuxt/@nuxt/cli/listhen/uqr": ["uqr@0.1.3", "", {}, "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA=="], + + "nuxt/@nuxt/cli/youch/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + "test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "vite-dev-rpc/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vite-dev-rpc/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "vite-hot-client/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vite-hot-client/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vite-hot-client/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vite-hot-client/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vite-hot-client/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vite-hot-client/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vite-hot-client/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vite-hot-client/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vite-hot-client/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vite-hot-client/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vite-hot-client/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vite-hot-client/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vite-hot-client/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vite-hot-client/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vite-hot-client/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vite-hot-client/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vite-hot-client/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vite-hot-client/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vite-hot-client/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vite-hot-client/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vite-hot-client/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vite-hot-client/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vite-hot-client/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "vite-node/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vite-node/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vite-node/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vite-node/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vite-node/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vite-node/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vite-node/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vite-node/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vite-node/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vite-node/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vite-node/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vite-node/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vite-node/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vite-node/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vite-node/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vite-node/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vite-node/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vite-node/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vite-node/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vite-node/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vite-node/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vite-node/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vite-node/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + "vitepress/@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="], "vitepress/@vue/devtools-api/@vue/devtools-kit/birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], @@ -4039,8 +4790,122 @@ "vitepress/@vue/devtools-api/@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + "vitepress/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vitepress/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vitepress/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vitepress/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vitepress/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vitepress/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vitepress/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vitepress/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vitepress/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vitepress/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vitepress/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vitepress/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vitepress/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vitepress/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vitepress/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vitepress/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vitepress/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vitepress/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vitepress/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vitepress/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vitepress/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vitepress/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vitepress/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "vitepress/vue/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], + + "vitepress/vue/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "vitepress/vue/@vue/runtime-dom/@vue/reactivity": ["@vue/reactivity@3.5.31", "", { "dependencies": { "@vue/shared": "3.5.31" } }, "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g=="], + + "vitepress/vue/@vue/runtime-dom/@vue/runtime-core": ["@vue/runtime-core@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q=="], + + "vitepress/vue/@vue/server-renderer/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], + + "vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + "@manypkg/find-root/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "@nuxt/devtools-kit/@nuxt/kit/c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "@nuxt/devtools-kit/@nuxt/kit/c12/giget/nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + + "@nuxt/devtools/@nuxt/kit/c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + + "@nuxt/vite-builder/cssnano/cssnano-preset-default/postcss-merge-longhand/stylehacks": ["stylehacks@7.0.11", "", { "dependencies": { "browserslist": "^4.28.2", "postcss-selector-parser": "^7.1.1" }, "peerDependencies": { "postcss": "^8.5.13" } }, "sha512-iODNfhXVLqc5LADs+Y6Oh5wJuK5ZcHbVng8aiK3y9pjMQdc5hLrBW0eFU6FtnpNrE6PoEg/MmFTU4waotj5WNg=="], + "@vercel/nft/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "@nuxt/devtools-kit/@nuxt/kit/c12/giget/nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + + "@nuxt/devtools-kit/@nuxt/kit/c12/giget/nypm/tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], } } diff --git a/package.json b/package.json index 0b2edaf..95b88eb 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,16 @@ "catalog": { "better-sqlite3": "^11.7.0", "@types/better-sqlite3": "^7.6.12", - "nuxt": "^4.0.0", + "nuxt": "^4.4.4", "vue": "^3.5.13", "vue-router": "^5.0.4", - "vite": "^5.4.14", - "@nuxt/kit": "^4.0.0", + "vite": "^8.0.10", + "svelte": "^5.55.5", + "next": "^16.2.4", + "@sveltejs/kit": "^2.59.1", + "@sveltejs/vite-plugin-svelte": "^7.1.0", + "@sveltejs/adapter-node": "^5.5.4", + "@nuxt/kit": "^4.4.4", "@nuxt/module-builder": "^1.0.2", "@eslint/js": "^9.17.0", "@types/node": "^22.10.2", diff --git a/packages/adapter-next/src/client.ts b/packages/adapter-next/src/client.ts index 0393c0e..8abbd47 100644 --- a/packages/adapter-next/src/client.ts +++ b/packages/adapter-next/src/client.ts @@ -1,8 +1,9 @@ import { useEffect, useRef, useState } from 'react' -import type { InferSchemaData } from '@holo-js/forms' +import type { FormSchema, InferFormData } from '@holo-js/forms' import { type ClientSubmitContext, type ClientSubmitResult, + type InferFormFieldTree, type UseFormOptions, type UseFormResult, useForm as createForm, @@ -50,7 +51,10 @@ function areEqual(left: unknown, right: unknown): boolean { return false } -function areOptionsEqual(left: UseFormOptions, right: UseFormOptions): boolean { +function areOptionsEqual( + left: UseFormOptions, + right: UseFormOptions, +): boolean { return left.action === right.action && left.method === right.method && left.csrf === right.csrf @@ -60,9 +64,11 @@ function areOptionsEqual(left: UseFormOptions, right: UseFormOptio && areEqual(left.initialState, right.initialState) } -function createSubmitterBridge( - optionsRef: { current: UseFormOptions | undefined }, -): ((context: ClientSubmitContext) => Promise> | ClientSubmitResult) { +function createSubmitterBridge( + optionsRef: { current: UseFormOptions | undefined }, +): ( + context: ClientSubmitContext, +) => Promise> | ClientSubmitResult { return (context) => { const submitter = optionsRef.current?.submitter @@ -74,26 +80,26 @@ function createSubmitterBridge( } } -export function useForm[0]>( +export function useForm( schemaDefinition: TSchema, - options: UseFormOptions> = {}, -): UseFormResult> { - type TData = InferSchemaData - - const formRef = useRef>() - const previousSchemaRef = useRef() - const previousOptionsRef = useRef>() - const latestOptionsRef = useRef>() - const submitterBridgeRef = useRef['submitter']>() + options: UseFormOptions, TSuccess> = {}, +): UseFormResult, TSuccess, InferFormFieldTree> { + type TData = InferFormData + + const formRef = useRef> | undefined>(undefined) + const previousSchemaRef = useRef(undefined) + const previousOptionsRef = useRef | undefined>(undefined) + const latestOptionsRef = useRef | undefined>(undefined) + const submitterBridgeRef = useRef['submitter']>(undefined) const [, setVersion] = useState(0) latestOptionsRef.current = options if (options.submitter && !submitterBridgeRef.current) { - submitterBridgeRef.current = createSubmitterBridge(latestOptionsRef) + submitterBridgeRef.current = createSubmitterBridge(latestOptionsRef) } - const resolvedOptions: UseFormOptions = options.submitter + const resolvedOptions: UseFormOptions = options.submitter ? { ...options, submitter: submitterBridgeRef.current, @@ -108,14 +114,14 @@ export function useForm[0]>( previousSchemaRef.current = schemaDefinition previousOptionsRef.current = options } else { - const previousOptions = previousOptionsRef.current as UseFormOptions + const previousOptions = previousOptionsRef.current as UseFormOptions if (!areOptionsEqual(previousOptions, options)) { formRef.current = createForm(schemaDefinition, resolvedOptions) previousOptionsRef.current = options } } - const form = formRef.current + const form = formRef.current! useEffect(() => form.subscribe(() => setVersion(version => version + 1)), [form]) diff --git a/packages/adapter-next/src/next-headers-shim.d.ts b/packages/adapter-next/src/next-headers-shim.d.ts new file mode 100644 index 0000000..75644a0 --- /dev/null +++ b/packages/adapter-next/src/next-headers-shim.d.ts @@ -0,0 +1,9 @@ +declare module 'next/headers' { + export function cookies(): Promise<{ + get(name: string): { + readonly value: string + } | undefined + }> + + export function headers(): Promise +} diff --git a/packages/adapter-next/src/runtime.ts b/packages/adapter-next/src/runtime.ts index e3de9c9..1311a11 100644 --- a/packages/adapter-next/src/runtime.ts +++ b/packages/adapter-next/src/runtime.ts @@ -1,3 +1,4 @@ +import { cookies, headers } from 'next/headers' import { initializeHolo, type CreateHoloOptions } from '@holo-js/core' import type { DotPath, HoloConfigMap, LoadedHoloConfig, ValueAtPath } from '@holo-js/config' @@ -5,10 +6,26 @@ export type NextHoloRuntimeOptions = CreateHoloOptions & { readonly projectRoot: string } +function resolveNextAuthRequestAccessors(): NonNullable { + return { + async getCookie(name: string) { + const store = await cookies() + return store.get(name)?.value + }, + async getHeader(name: string) { + const requestHeaders = await headers() + return requestHeaders.get(name) ?? undefined + }, + } +} + export function createNextHoloHelpers( options: NextHoloRuntimeOptions, ) { - const resolveRuntime = async () => await initializeHolo(options.projectRoot, options) + const resolveRuntime = async () => await initializeHolo(options.projectRoot, { + ...options, + authRequest: options.authRequest ?? resolveNextAuthRequestAccessors(), + }) const useConfig = async ['all']>>( path: TPath, diff --git a/packages/adapter-next/tsup.config.ts b/packages/adapter-next/tsup.config.ts index ed9ff36..563942e 100644 --- a/packages/adapter-next/tsup.config.ts +++ b/packages/adapter-next/tsup.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ clean: true, outDir: 'dist', outExtension: () => ({ js: '.mjs' }), - external: ['react'], + external: ['react', 'next/headers'], esbuildOptions(options) { options.logLevel = 'warning' }, diff --git a/packages/adapter-nuxt/package.json b/packages/adapter-nuxt/package.json index 8789330..c91120c 100644 --- a/packages/adapter-nuxt/package.json +++ b/packages/adapter-nuxt/package.json @@ -34,7 +34,7 @@ "test": "vitest --run" }, "dependencies": { - "@nuxt/kit": "^4.0.0", + "@nuxt/kit": "catalog:", "@holo-js/config": "^0.1.4", "@holo-js/core": "^0.1.4", "@holo-js/db": "^0.1.4" @@ -59,7 +59,7 @@ "@holo-js/storage-s3": "workspace:*", "@nuxt/module-builder": "^1.0.2", "@types/node": "^22.10.2", - "nuxt": "^4.0.0", + "nuxt": "catalog:", "typescript": "^5.7.2", "vitest": "^2.1.8" } diff --git a/packages/adapter-nuxt/src/module.ts b/packages/adapter-nuxt/src/module.ts index e8159fc..3631b2b 100644 --- a/packages/adapter-nuxt/src/module.ts +++ b/packages/adapter-nuxt/src/module.ts @@ -119,6 +119,10 @@ interface NuxtHookContext { interface NuxtOptionsWithNitro { nitro: { storage: Record + experimental?: { + asyncContext?: boolean + [key: string]: unknown + } [key: string]: unknown } runtimeConfig: { @@ -273,6 +277,10 @@ export default defineNuxtModule({ opts.nitro = opts.nitro || { storage: {} } opts.nitro.storage = opts.nitro.storage || {} + opts.nitro.experimental = { + ...(opts.nitro.experimental || {}), + asyncContext: true, + } opts.runtimeConfig = opts.runtimeConfig || {} opts.runtimeConfig.public = opts.runtimeConfig.public || {} opts.runtimeConfig.public.holo = { @@ -308,15 +316,15 @@ export default defineNuxtModule({ if (!opts._holoCoreRuntimeRegistered) { const imports = [ - { name: 'holo', as: 'holo', from: resolver.resolve('./runtime/composables') }, - { name: 'useHoloDb', as: 'useHoloDb', from: resolver.resolve('./runtime/composables') }, - { name: 'useHoloEnv', as: 'useHoloEnv', from: resolver.resolve('./runtime/composables') }, - { name: 'useHoloDebug', as: 'useHoloDebug', from: resolver.resolve('./runtime/composables') }, + { name: 'holo', as: 'holo', from: '@holo-js/adapter-nuxt/runtime' }, + { name: 'useHoloDb', as: 'useHoloDb', from: '@holo-js/adapter-nuxt/runtime' }, + { name: 'useHoloEnv', as: 'useHoloEnv', from: '@holo-js/adapter-nuxt/runtime' }, + { name: 'useHoloDebug', as: 'useHoloDebug', from: '@holo-js/adapter-nuxt/runtime' }, ] if (storageModule) { imports.push( - { name: 'useStorage', as: 'useStorage', from: resolver.resolve('./runtime/composables/storage') }, - { name: 'Storage', as: 'Storage', from: resolver.resolve('./runtime/composables/storage') }, + { name: 'useStorage', as: 'useStorage', from: '@holo-js/adapter-nuxt/storage' }, + { name: 'Storage', as: 'Storage', from: '@holo-js/adapter-nuxt/storage' }, ) } addServerPlugin(resolver.resolve('./runtime/plugins/init')) diff --git a/packages/adapter-nuxt/src/runtime/composables/forms.ts b/packages/adapter-nuxt/src/runtime/composables/forms.ts index 7910bda..df7ac33 100644 --- a/packages/adapter-nuxt/src/runtime/composables/forms.ts +++ b/packages/adapter-nuxt/src/runtime/composables/forms.ts @@ -1,11 +1,11 @@ import { onScopeDispose, shallowRef, watchEffect } from 'vue' -import type { FormSchema } from '@holo-js/forms' +import type { FormSchema, InferFormData } from '@holo-js/forms' import { + type InferFormFieldTree, type UseFormOptions, type UseFormResult, useForm as createForm, } from '@holo-js/forms/client' -import type { InferSchemaData, SchemaInputShape } from '@holo-js/validation' export { type ClientSubmitContext, @@ -89,10 +89,10 @@ function createReactiveView( return proxy as TValue } -export function useForm>( +export function useForm( schemaDefinition: TSchema, - options: UseFormOptions> = {}, -): UseFormResult> { + options: UseFormOptions, TSuccess> = {}, +): UseFormResult, TSuccess, InferFormFieldTree> { const form = shallowRef(createForm(schemaDefinition, options)) const version = shallowRef(0) const cache = new WeakMap() diff --git a/packages/adapter-nuxt/src/runtime/composables/index.ts b/packages/adapter-nuxt/src/runtime/composables/index.ts index 6e48503..ed89fe6 100644 --- a/packages/adapter-nuxt/src/runtime/composables/index.ts +++ b/packages/adapter-nuxt/src/runtime/composables/index.ts @@ -2,6 +2,7 @@ import type { RuntimeConnectionConfig, RuntimeDatabaseConfig } from '@holo-js/db import { createHoloProjectAccessors, initializeHoloAdapterProject, + type CreateHoloOptions, } from '@holo-js/core' type RuntimeConfigShape = { @@ -19,6 +20,22 @@ type RuntimeGlobals = typeof globalThis & { useRuntimeConfig?: () => RuntimeConfigShape } +type NuxtAuthRequestEvent = { + readonly headers?: Headers + readonly request?: { + readonly headers?: Headers + } + readonly node?: { + readonly req?: { + readonly headers?: Readonly> + } + } +} + +type NitroContextModule = { + readonly useEvent: () => NuxtAuthRequestEvent | undefined +} + export function configureHoloRuntimeConfig(config: RuntimeConfigShape): void { const runtimeGlobals = globalThis as RuntimeGlobals runtimeGlobals.__holoRuntimeConfig = config @@ -51,6 +68,92 @@ function resolveRuntimeProjectRoot(config: RuntimeConfigShape): string { return config.holo.projectRoot?.trim() || process.cwd() } +async function loadNitroContextModule(): Promise { + return await import('nitropack/runtime/context') as NitroContextModule +} + +export function createNuxtAuthRequestAccessors() { + function safeDecode(value: string): string | undefined { + try { + return decodeURIComponent(value) + } catch { + return undefined + } + } + + async function readHeader(name: string): Promise { + const nitroContext = await loadNitroContextModule() + const event = nitroContext.useEvent() + + if (!event) { + return undefined + } + + const normalizedName = name.toLowerCase() + + if (event.headers instanceof Headers) { + return event.headers.get(name) ?? undefined + } + + if (event.request?.headers instanceof Headers) { + return event.request.headers.get(name) ?? undefined + } + + const value = event.node?.req?.headers?.[normalizedName] + if (Array.isArray(value)) { + return value[0] + } + + return typeof value === 'string' ? value : undefined + } + + async function readCookie(name: string): Promise { + const header = await readHeader('cookie') + if (!header) { + return undefined + } + + for (const segment of header.split(';')) { + const trimmed = segment.trim() + const separator = trimmed.indexOf('=') + if (separator <= 0) { + continue + } + + const key = safeDecode(trimmed.slice(0, separator)) + if (typeof key === 'undefined') { + continue + } + + if (key !== name) { + continue + } + + const value = safeDecode(trimmed.slice(separator + 1)) + if (typeof value === 'undefined') { + continue + } + + return value + } + + return undefined + } + + const getCookieValue: NonNullable['getCookie'] = async (name) => { + return await readCookie(name) + } + + const getHeaderValue: NonNullable['getHeader'] = async (name) => { + return await readHeader(name) + } + + return { + getCookie: getCookieValue, + getHeader: getHeaderValue, + } satisfies NonNullable +} + export const holo = createHoloProjectAccessors(async () => { const config = getRuntimeConfig() @@ -58,6 +161,7 @@ export const holo = createHoloProjectAccessors(async () => { envName: resolveRuntimeEnvName(config.holo.appEnv), preferCache: process.env.NODE_ENV === 'production', processEnv: process.env, + authRequest: createNuxtAuthRequestAccessors(), }) }) diff --git a/packages/adapter-nuxt/src/runtime/plugins/init.ts b/packages/adapter-nuxt/src/runtime/plugins/init.ts index b2fc875..21056b4 100644 --- a/packages/adapter-nuxt/src/runtime/plugins/init.ts +++ b/packages/adapter-nuxt/src/runtime/plugins/init.ts @@ -1,4 +1,5 @@ import { + createNuxtAuthRequestAccessors, configureHoloRuntimeConfig, } from '../composables' import { initializeHoloAdapterProject } from '@holo-js/core' @@ -13,6 +14,7 @@ export default defineNitroPlugin(async (nitroApp: { hooks: { hook: (name: string envName: config.holo.appEnv, preferCache: process.env.NODE_ENV === 'production', processEnv: process.env, + authRequest: createNuxtAuthRequestAccessors(), }) const driver = project.runtime.manager.connection().getDriver() diff --git a/packages/adapter-nuxt/src/runtime/shims.d.ts b/packages/adapter-nuxt/src/runtime/shims.d.ts index 513d1ca..0ee7e05 100644 --- a/packages/adapter-nuxt/src/runtime/shims.d.ts +++ b/packages/adapter-nuxt/src/runtime/shims.d.ts @@ -47,6 +47,10 @@ declare module 'nitropack/runtime/config' { export function useRuntimeConfig(): HoloRuntimeConfig } +declare module 'nitropack/runtime/context' { + export function useEvent(): unknown +} + declare module 'nitropack/runtime/plugin' { export function defineNitroPlugin(plugin: T): T } diff --git a/packages/adapter-nuxt/tests/module.test.ts b/packages/adapter-nuxt/tests/module.test.ts index 290c5b5..41eb127 100644 --- a/packages/adapter-nuxt/tests/module.test.ts +++ b/packages/adapter-nuxt/tests/module.test.ts @@ -175,6 +175,7 @@ afterEach(() => { vi.doUnmock('#app') vi.doUnmock('nitropack/runtime/plugin') vi.doUnmock('nitropack/runtime/config') + vi.doUnmock('nitropack/runtime/context') vi.doUnmock('@holo-js/config') vi.doUnmock('@holo-js/db') vi.doUnmock('@holo-js/core') @@ -350,6 +351,25 @@ export default defineDatabaseConfig({ expect(references).toContainEqual({ types: '@holo-js/adapter-nuxt' }) }) + it('skips malformed cookie segments when decoding auth cookies', async () => { + vi.resetModules() + vi.doMock('nitropack/runtime/context', () => ({ + useEvent: () => ({ + request: { + headers: new Headers({ + cookie: 'broken=%; auth-token=secret%20value', + }), + }, + }), + })) + + const runtime = await import('../src/runtime/composables') + const accessors = runtime.createNuxtAuthRequestAccessors() + + await expect(accessors.getCookie('broken')).resolves.toBeUndefined() + await expect(accessors.getCookie('auth-token')).resolves.toBe('secret value') + }) + it('generates a server import wrapper for model defaults when server models exist', async () => { const root = await createProject() await writeFile(join(root, 'config/app.ts'), ` @@ -1034,11 +1054,11 @@ describe('useHoloDb', () => { projectRoot: '/tmp/nuxt-project', }) expect(runtime.useHoloEnv()).toBe('test') - expect(initializeHoloAdapterProject).toHaveBeenCalledWith('/tmp/nuxt-project', { + expect(initializeHoloAdapterProject).toHaveBeenCalledWith('/tmp/nuxt-project', expect.objectContaining({ envName: 'test', preferCache: true, processEnv: process.env, - }) + })) } finally { vi.unstubAllEnvs() cwd.mockRestore() @@ -1177,11 +1197,11 @@ describe('runtime plugin', () => { }, }) expect(initializeHoloAdapterProject).toHaveBeenCalledTimes(1) - expect(initializeHoloAdapterProject).toHaveBeenCalledWith('/tmp/nuxt-project', { + expect(initializeHoloAdapterProject).toHaveBeenCalledWith('/tmp/nuxt-project', expect.objectContaining({ envName: 'development', preferCache: false, processEnv: process.env, - }) + })) expect(log).toHaveBeenCalledWith('✅ Holo DB connected (sqlite)') const closeHandler = hook.mock.calls.find(([name]) => name === 'close')?.[1] diff --git a/packages/adapter-nuxt/tsconfig.json b/packages/adapter-nuxt/tsconfig.json index 037f1f9..4c463e7 100644 --- a/packages/adapter-nuxt/tsconfig.json +++ b/packages/adapter-nuxt/tsconfig.json @@ -8,6 +8,7 @@ "#app": ["./src/runtime/shims.d.ts"], "#imports": ["./src/runtime/shims.d.ts"], "nitropack/runtime/config": ["./src/runtime/shims.d.ts"], + "nitropack/runtime/context": ["./src/runtime/shims.d.ts"], "nitropack/runtime/plugin": ["./src/runtime/shims.d.ts"], "nitropack/runtime/storage": ["./src/runtime/shims.d.ts"], "@holo-js/forms": ["../forms/src/index.ts"], diff --git a/packages/adapter-sveltekit/package.json b/packages/adapter-sveltekit/package.json index 90ac17f..a11ebc7 100644 --- a/packages/adapter-sveltekit/package.json +++ b/packages/adapter-sveltekit/package.json @@ -34,7 +34,8 @@ }, "dependencies": { "@holo-js/config": "^0.1.4", - "@holo-js/core": "^0.1.4" + "@holo-js/core": "^0.1.4", + "svelte": "catalog:" }, "peerDependencies": { "@holo-js/forms": "^0.1.4" diff --git a/packages/adapter-sveltekit/src/client.ts b/packages/adapter-sveltekit/src/client.ts index 7501704..ab4dbe2 100644 --- a/packages/adapter-sveltekit/src/client.ts +++ b/packages/adapter-sveltekit/src/client.ts @@ -1,6 +1,7 @@ import { createSubscriber } from 'svelte/reactivity' -import type { InferSchemaData } from '@holo-js/forms' +import type { FormSchema, InferFormData } from '@holo-js/forms' import { + type InferFormFieldTree, type UseFormOptions, type UseFormResult, useForm as createForm, @@ -80,15 +81,15 @@ function createReactiveView( return proxy as TValue } -export function useForm[0]>( +export function useForm( schemaDefinition: TSchema, - options: UseFormOptions> = {}, -): UseFormResult> { - type TData = InferSchemaData + options: UseFormOptions, TSuccess> = {}, +): UseFormResult, TSuccess, InferFormFieldTree> { + type TData = InferFormData const form = createForm(schemaDefinition, options) const subscribe = createSubscriber((update) => form.subscribe(update)) const cache = new WeakMap() - return createReactiveView>(form, subscribe, cache) + return createReactiveView>>(form, subscribe, cache) } diff --git a/packages/adapter-sveltekit/src/index.ts b/packages/adapter-sveltekit/src/index.ts index ff159da..62aa103 100644 --- a/packages/adapter-sveltekit/src/index.ts +++ b/packages/adapter-sveltekit/src/index.ts @@ -3,6 +3,7 @@ import { type HoloAdapterProject, type HoloFrameworkOptions, } from '@holo-js/core' +import { getRequestEvent } from '$app/server' import type { HoloConfigMap } from '@holo-js/config' export { holoSvelteKitTransport, @@ -13,6 +14,26 @@ export type SvelteKitHoloOptions = HoloFrameworkOptions export type SvelteKitHoloProject = HoloAdapterProject +function withSvelteKitAuthRequest(options: SvelteKitHoloOptions = {}): SvelteKitHoloOptions { + if (options.authRequest) { + return options + } + + return { + ...options, + authRequest: { + async getCookie(name: string) { + const event = getRequestEvent() + return event.cookies.get(name) ?? undefined + }, + async getHeader(name: string) { + const event = getRequestEvent() + return event.request.headers.get(name) ?? undefined + }, + }, + } +} + const svelteKitAdapter = createHoloFrameworkAdapter({ stateKey: '__holoSvelteKitAdapter__', displayName: 'SvelteKit', @@ -23,19 +44,19 @@ export const svelteKitHoloCapabilities = svelteKitAdapter.capabilities export async function createSvelteKitHoloProject( options: SvelteKitHoloOptions = {}, ): Promise> { - return svelteKitAdapter.createProject(options) + return svelteKitAdapter.createProject(withSvelteKitAuthRequest(options)) } export async function initializeSvelteKitHoloProject( options: SvelteKitHoloOptions = {}, ): Promise> { - return svelteKitAdapter.initializeProject(options) + return svelteKitAdapter.initializeProject(withSvelteKitAuthRequest(options)) } export function createSvelteKitHoloHelpers( options: SvelteKitHoloOptions = {}, ) { - return svelteKitAdapter.createHelpers(options) + return svelteKitAdapter.createHelpers(withSvelteKitAuthRequest(options)) } export async function resetSvelteKitHoloProject(): Promise { diff --git a/packages/adapter-sveltekit/src/sveltekit.d.ts b/packages/adapter-sveltekit/src/sveltekit.d.ts new file mode 100644 index 0000000..4d79019 --- /dev/null +++ b/packages/adapter-sveltekit/src/sveltekit.d.ts @@ -0,0 +1,10 @@ +declare module '$app/server' { + export function getRequestEvent(): { + readonly cookies: { + get(name: string): string | undefined + } + readonly request: { + readonly headers: Headers + } + } +} diff --git a/packages/adapter-sveltekit/tests/adapter.test.ts b/packages/adapter-sveltekit/tests/adapter.test.ts index 2840ab5..88db61a 100644 --- a/packages/adapter-sveltekit/tests/adapter.test.ts +++ b/packages/adapter-sveltekit/tests/adapter.test.ts @@ -121,6 +121,20 @@ async function loadAdapterModule() { if (!adapterModulePromise) { const { coreEntryUrl } = await ensureIsolatedCoreBuild() vi.doMock('@holo-js/core', () => import(coreEntryUrl)) + vi.doMock('$app/server', () => ({ + getRequestEvent() { + return { + cookies: { + get() { + return undefined + }, + }, + request: { + headers: new Headers(), + }, + } + }, + })) adapterModulePromise = import('../src') } @@ -173,6 +187,7 @@ afterEach(async () => { await resetSvelteKitHoloProject() adapterModulePromise = null vi.doUnmock('@holo-js/core') + vi.doUnmock('$app/server') vi.resetModules() await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true }))) }, 45000) diff --git a/packages/auth/src/client-runtime.ts b/packages/auth/src/client-runtime.ts index 7a40908..2f561b2 100644 --- a/packages/auth/src/client-runtime.ts +++ b/packages/auth/src/client-runtime.ts @@ -142,6 +142,25 @@ export async function user(options?: AuthClientRequestOptions): Promise boolean + readonly refreshUser: () => Promise +}> { + const current = await fetchCurrentUser(options) + + return Object.freeze({ + ...current, + check() { + return current.authenticated + }, + refreshUser() { + return refreshUser(options) + }, + }) +} + export async function refreshUser(options?: AuthClientRequestOptions): Promise { return (await fetchCurrentUser(options, { force: true })).user } diff --git a/packages/auth/src/client.ts b/packages/auth/src/client.ts index bcebd38..ac78cc6 100644 --- a/packages/auth/src/client.ts +++ b/packages/auth/src/client.ts @@ -1,10 +1,11 @@ -import { check, refreshUser, user } from './client-runtime' +import { check, refreshUser, useAuth, user } from './client-runtime' -export { authClientInternals, check, configureAuthClient, refreshUser, resetAuthClient, user } from './client-runtime' +export { authClientInternals, check, configureAuthClient, refreshUser, resetAuthClient, useAuth, user } from './client-runtime' export type { AuthClientConfig, AuthClientRequestOptions, AuthUser, AuthUserLike, CurrentAuthResponse, HoloAuthTypeRegistry } from './contracts' const auth = Object.freeze({ check, + useAuth, user, refreshUser, }) diff --git a/packages/auth/src/contracts.ts b/packages/auth/src/contracts.ts index 0557c67..cdfdd29 100644 --- a/packages/auth/src/contracts.ts +++ b/packages/auth/src/contracts.ts @@ -2,9 +2,147 @@ export { defineAuthConfig } from '@holo-js/config' export type { AuthGuardConfig, AuthProviderConfig, HoloAuthConfig, NormalizedHoloAuthConfig } from '@holo-js/config' import type { HoloAuthConfig, NormalizedHoloAuthConfig } from '@holo-js/config' +export const AUTH_ERROR_CODES = [ + 'runtime_unconfigured', + 'token_runtime_unconfigured', + 'email_verification_runtime_unconfigured', + 'password_reset_runtime_unconfigured', + 'guard_not_configured', + 'provider_not_configured', + 'provider_runtime_not_configured', + 'guard_session_login_unsupported', + 'credentials_identifier_missing', + 'password_confirmation_mismatch', + 'invalid_credentials', + 'email_verification_required', + 'trusted_login_user_required', + 'trusted_login_provider_mismatch', + 'trusted_login_user_not_found', + 'trusted_login_user_incompatible', + 'impersonation_actor_required', + 'impersonation_nested_unsupported', + 'impersonation_already_active', + 'registration_identifier_taken', + 'auth_user_missing', + 'provider_resolution_required', + 'provider_update_unsupported', + 'email_required_for_verification', + 'email_verification_user_missing', + 'email_already_verified', + 'email_verification_token_invalid', + 'email_verification_token_expired', + 'password_reset_email_required', + 'password_broker_not_configured', + 'password_reset_token_invalid', + 'password_reset_token_expired', + 'password_reset_user_missing', +] as const + +export type AuthErrorCode = typeof AUTH_ERROR_CODES[number] + +export interface AuthErrorOptions { + readonly cause?: unknown + readonly details?: Readonly> +} + +export class AuthError extends Error { + readonly code: TCode + readonly details?: Readonly> + + constructor(code: TCode, message: string, options: AuthErrorOptions = {}) { + super(message) + this.name = 'AuthError' + this.code = code + this.details = options.details + + if ('cause' in options) { + this.cause = options.cause + } + } +} + +export function isAuthError(value: unknown): value is AuthError { + return value instanceof AuthError + || ( + !!value + && typeof value === 'object' + && (value as { name?: unknown }).name === 'AuthError' + && typeof (value as { code?: unknown }).code === 'string' + && (AUTH_ERROR_CODES as readonly string[]).includes((value as { code: string }).code) + && typeof (value as { message?: unknown }).message === 'string' + ) +} + +export type AuthFieldErrors = Partial> + +export type AuthInputFieldErrors< + TInput extends Readonly>, + TExtraField extends string = never, +> = AuthFieldErrors | TExtraField> + +export interface AuthFailure { + readonly code: TCode + readonly message: string + readonly status: number + readonly fields: TFields +} + +export interface AuthSuccessResult { + readonly data: TData + readonly error: null +} + +export interface AuthFailureResult< + TCode extends AuthErrorCode = AuthErrorCode, + TFields extends AuthFieldErrors = AuthFieldErrors, +> { + readonly data: null + readonly error: AuthFailure +} + +export type AuthResult< + TData, + TCode extends AuthErrorCode = AuthErrorCode, + TFields extends AuthFieldErrors = AuthFieldErrors, +> + = AuthSuccessResult + | AuthFailureResult + +export type AuthLoginErrorCode + = 'credentials_identifier_missing' + | 'invalid_credentials' + | 'email_verification_required' + +export type AuthRegistrationErrorCode + = 'credentials_identifier_missing' + | 'password_confirmation_mismatch' + | 'registration_identifier_taken' + +export type AuthEmailVerificationConsumeErrorCode + = 'email_verification_token_invalid' + | 'email_verification_token_expired' + | 'auth_user_missing' + | 'provider_update_unsupported' + +export type AuthEmailVerificationResendErrorCode + = 'email_verification_user_missing' + | 'email_already_verified' + +export type AuthPasswordResetRequestErrorCode + = 'password_reset_email_required' + +export type AuthPasswordResetConsumeErrorCode + = 'password_confirmation_mismatch' + | 'password_reset_token_invalid' + | 'password_reset_token_expired' + | 'password_reset_user_missing' + | 'auth_user_missing' + | 'provider_update_unsupported' + export interface AuthUserLike { readonly id?: string | number readonly email?: string + readonly name?: string readonly [key: string]: unknown } @@ -28,6 +166,21 @@ export interface AuthRegistrationInput extends AuthCredentials { readonly passwordConfirmation: string } +export interface AuthPasswordResetRequestInput extends Readonly> { + readonly email: string +} + +export interface AuthPasswordResetInput extends Readonly> { + readonly token: string + readonly password: string + readonly passwordConfirmation: string +} + +export interface AuthPasswordResetRequestOptions { + readonly broker?: string + readonly expiresAt?: Date +} + export interface AuthSessionLoginOptions { readonly remember?: boolean } @@ -56,7 +209,9 @@ export interface AuthGuardFacade { refreshUser(): Promise id(): Promise currentAccessToken(): Promise - login(credentials: AuthCredentials): Promise + login( + credentials: TCredentials, + ): Promise>> loginUsing(user: unknown, options?: AuthSessionLoginOptions): Promise loginUsingId(userId: string | number, options?: AuthSessionLoginOptions): Promise impersonate(user: unknown, options?: AuthImpersonationOptions): Promise @@ -67,14 +222,22 @@ export interface AuthGuardFacade { } export interface AuthFacade extends AuthGuardFacade { - register(input: AuthRegistrationInput): Promise + register( + input: TInput, + ): Promise>> + requestPasswordReset( + input: TInput, + options?: AuthPasswordResetRequestOptions, + ): Promise>> + resetPassword( + input: TInput, + ): Promise>> hashPassword(password: string): Promise verifyPassword(password: string, digest: string): Promise needsPasswordRehash(digest: string): Promise guard(name: string): AuthGuardFacade tokens: AuthTokenFacade verification: AuthEmailVerificationFacade - passwords: AuthPasswordResetFacade } type AuthProviderAdapterBase = { @@ -225,26 +388,21 @@ export interface AuthDeliveryHook { readonly user: AuthUser readonly email: string readonly token: EmailVerificationTokenResult + readonly route: string }): Promise sendPasswordReset(input: { + readonly broker: string readonly provider: string readonly email: string readonly token: PasswordResetTokenResult + readonly route: string }): Promise } export interface AuthEmailVerificationFacade { create(user: unknown, options?: { readonly guard?: string, readonly expiresAt?: Date }): Promise - consume(plainTextToken: string): Promise -} - -export interface AuthPasswordResetFacade { - request(email: string, options?: { readonly broker?: string, readonly expiresAt?: Date }): Promise - consume(input: { - readonly token: string - readonly password: string - readonly passwordConfirmation: string - }): Promise + resend(options?: { readonly guard?: string, readonly expiresAt?: Date, readonly email?: string }): Promise>> + consume(plainTextToken: string): Promise>> } export interface AuthSessionRecord { @@ -300,6 +458,8 @@ export interface AuthRuntimeContext { setSessionId(guardName: string, sessionId?: string): void getCachedUser(guardName: string): AuthUser | null | undefined setCachedUser(guardName: string, user: AuthUser | null): void + getRequestCookie?(name: string): string | undefined | Promise + getRequestHeader?(name: string): string | undefined | Promise getAccessToken?(guardName: string): string | undefined setAccessToken?(guardName: string, token?: string): void getRememberToken?(guardName: string): string | undefined @@ -328,6 +488,8 @@ export interface AuthEstablishedSession { readonly sessionId: string readonly rememberToken?: string readonly cookies: readonly string[] + readonly emailVerificationRequired?: boolean + readonly emailVerificationRoute?: string } export interface CurrentAuthResponse { diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index c866881..9927f28 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -1,6 +1,6 @@ -import { check, currentAccessToken, getAuthRuntime, hashPassword, id, impersonate, impersonateById, impersonation, login, loginUsing, loginUsingId, logout, needsPasswordRehash, passwords, refreshUser, register, stopImpersonating, tokens, user, verification, verifyPassword } from './runtime' +import { check, currentAccessToken, getAuthRuntime, hashPassword, id, impersonate, impersonateById, impersonation, login, loginUsing, loginUsingId, logout, needsPasswordRehash, refreshUser, register, requestPasswordReset, resetPassword, stopImpersonating, tokens, user, verification, verifyPassword } from './runtime' -export { defineAuthConfig } from './contracts' +export { AUTH_ERROR_CODES, AuthError, defineAuthConfig, isAuthError } from './contracts' export { authRuntimeInternals, check, @@ -18,10 +18,11 @@ export { loginUsingId, logout, needsPasswordRehash, - passwords, refreshUser, register, + requestPasswordReset, resetAuthRuntime, + resetPassword, stopImpersonating, tokens, user, @@ -29,21 +30,36 @@ export { verifyPassword, } from './runtime' export type { + AuthFailure, AuthClientConfig, AuthClientRequestOptions, AuthCredentials, AuthCurrentAccessToken, AuthDeliveryHook, + AuthErrorCode, + AuthEmailVerificationConsumeErrorCode, AuthEmailVerificationFacade, + AuthEmailVerificationResendErrorCode, AuthEstablishedSession, AuthFacade, + AuthFieldErrors, + AuthFailureResult, AuthGuardFacade, + AuthInputFieldErrors, AuthImpersonationOptions, AuthImpersonationState, + AuthLoginErrorCode, AuthLogoutResult, AuthSessionLoginOptions, AuthPasswordHasher, - AuthPasswordResetFacade, + AuthPasswordResetInput, + AuthPasswordResetConsumeErrorCode, + AuthPasswordResetRequestInput, + AuthPasswordResetRequestErrorCode, + AuthPasswordResetRequestOptions, + AuthRegistrationErrorCode, + AuthResult, + AuthSuccessResult, AuthTokenFacade, AuthTokenStore, AuthUser, @@ -60,6 +76,7 @@ export type { EmailVerificationTokenStore, HoloAuthTypeRegistry, HoloAuthConfig, + AuthErrorOptions, NormalizedHoloAuthConfig, PersonalAccessTokenCreationOptions, PersonalAccessTokenRecord, @@ -85,11 +102,12 @@ const auth = Object.freeze({ logout, needsPasswordRehash, register, + requestPasswordReset, + resetPassword, stopImpersonating, tokens, verification, verifyPassword, - passwords, guard(name: string) { return getAuthRuntime().guard(name) }, diff --git a/packages/auth/src/runtime.ts b/packages/auth/src/runtime.ts index f42bc18..026cc37 100644 --- a/packages/auth/src/runtime.ts +++ b/packages/auth/src/runtime.ts @@ -3,20 +3,34 @@ import { createHash, createHmac, randomBytes, randomUUID, scrypt as nodeScrypt, import { promisify } from 'node:util' import { normalizeAuthConfig } from '@holo-js/config' import type { + AuthFailure, + AuthErrorCode, + AuthFieldErrors, AuthCredentials, AuthCurrentAccessToken, AuthDeliveryHook, + AuthEmailVerificationConsumeErrorCode, AuthEmailVerificationFacade, + AuthEmailVerificationResendErrorCode, AuthEstablishedSession, AuthFacade, + AuthFailureResult, AuthGuardFacade, AuthImpersonationOptions, AuthImpersonationState, + AuthLoginErrorCode, AuthLogoutResult, - AuthPasswordResetFacade, + AuthPasswordResetInput, + AuthPasswordResetRequestInput, + AuthPasswordResetRequestOptions, AuthPasswordHasher, + AuthPasswordResetConsumeErrorCode, + AuthPasswordResetRequestErrorCode, + AuthRegistrationErrorCode, + AuthResult, AuthRegistrationInput, AuthSessionLoginOptions, + AuthSuccessResult, AuthTokenFacade, AuthTokenStore, AuthUser, @@ -33,6 +47,7 @@ import type { PasswordResetTokenRecord, PasswordResetTokenStore, } from './contracts' +import { AuthError, isAuthError } from './contracts' const scrypt = promisify(nodeScrypt) const SCRYPT_PREFIX = 'scrypt' @@ -118,6 +133,316 @@ type OptionalSecurityModule = { let optionalSecurityModulePromise: Promise | undefined +function createAuthError( + code: AuthErrorCode, + message: string, + details?: Readonly>, +): AuthError { + return new AuthError(code, message, { + details, + }) +} + +function throwAuthError( + code: AuthErrorCode, + message: string, + details?: Readonly>, +): never { + throw createAuthError(code, message, details) +} + +function createAuthSuccess(data: TData): AuthSuccessResult { + return Object.freeze({ + data, + error: null, + }) +} + +function createAuthFailure( + error: AuthFailure, +): AuthFailureResult { + return Object.freeze({ + data: null, + error, + }) +} + +async function captureExpectedAuthResult( + operation: () => Promise, + expectedCodes: readonly TCode[], + mapError: (error: AuthError) => AuthFailure, +): Promise> { + try { + return createAuthSuccess(await operation()) + } catch (error) { + if (isAuthError(error) && expectedCodes.includes(error.code as TCode)) { + return createAuthFailure(mapError(error as AuthError)) + } + + throw error + } +} + +const EXPECTED_LOGIN_ERRORS = [ + 'credentials_identifier_missing', + 'invalid_credentials', + 'email_verification_required', +] as const satisfies readonly AuthLoginErrorCode[] + +const EXPECTED_REGISTRATION_ERRORS = [ + 'credentials_identifier_missing', + 'password_confirmation_mismatch', + 'registration_identifier_taken', +] as const satisfies readonly AuthRegistrationErrorCode[] + +const EXPECTED_EMAIL_VERIFICATION_CONSUME_ERRORS = [ + 'email_verification_token_invalid', + 'email_verification_token_expired', + 'auth_user_missing', + 'provider_update_unsupported', +] as const satisfies readonly AuthEmailVerificationConsumeErrorCode[] + +const EXPECTED_EMAIL_VERIFICATION_RESEND_ERRORS = [ + 'email_verification_user_missing', + 'email_already_verified', +] as const satisfies readonly AuthEmailVerificationResendErrorCode[] + +const EXPECTED_PASSWORD_RESET_REQUEST_ERRORS = [ + 'password_reset_email_required', +] as const satisfies readonly AuthPasswordResetRequestErrorCode[] + +const EXPECTED_PASSWORD_RESET_CONSUME_ERRORS = [ + 'password_confirmation_mismatch', + 'password_reset_token_invalid', + 'password_reset_token_expired', + 'password_reset_user_missing', + 'auth_user_missing', + 'provider_update_unsupported', +] as const satisfies readonly AuthPasswordResetConsumeErrorCode[] + +type InputFieldName>> = Extract + +function hasInputField>>( + input: TInput, + field: string, +): field is InputFieldName { + return field in input +} + +function pickInputField>>( + input: TInput, + candidates: readonly string[], +): InputFieldName | undefined { + for (const candidate of candidates) { + if (hasInputField(input, candidate)) { + return candidate + } + } + + const [firstField] = Object.keys(input) + return firstField && hasInputField(input, firstField) ? firstField : undefined +} + +function createFieldErrors( + fields: readonly TField[], + message: string, +): AuthFieldErrors { + return Object.freeze( + Object.fromEntries(fields.map(field => [field, [message] as readonly string[]])) as AuthFieldErrors, + ) +} + +function createAuthFailurePayload( + code: TCode, + message: string, + status: number, + fields: TFields, +): AuthFailure { + return Object.freeze({ + code, + message, + status, + fields, + }) +} + +function resolveIdentifierFieldName>>( + input: TInput, + error: AuthError, +): InputFieldName | undefined { + const identifier = error.details?.identifier + if (typeof identifier === 'string' && hasInputField(input, identifier)) { + return identifier + } + + return pickInputField(input, ['email', 'username', 'phone']) +} + +function resolveRequiredFieldName>>( + input: TInput, + candidates: readonly string[], +): InputFieldName { + const field = pickInputField(input, candidates) + if (!field) { + throw new Error('[@holo-js/auth] Expected auth failure mapping to resolve at least one input field.') + } + + return field +} + +function createLoginFailure( + error: AuthError, + credentials: TCredentials, +): AuthFailure, readonly string[]>>> { + switch (error.code) { + case 'invalid_credentials': { + const message = 'These credentials do not match our records.' + const fields = [ + resolveIdentifierFieldName(credentials, error), + hasInputField(credentials, 'password') ? 'password' : undefined, + ].filter((field): field is InputFieldName => typeof field === 'string') + + return createAuthFailurePayload( + error.code, + message, + 401, + createFieldErrors(fields.length > 0 ? fields : [resolveRequiredFieldName(credentials, ['password'])], message), + ) + } + + case 'email_verification_required': { + const message = 'Verify your email address before signing in.' + const field = resolveIdentifierFieldName(credentials, error) ?? resolveRequiredFieldName(credentials, ['password']) + return createAuthFailurePayload(error.code, message, 403, createFieldErrors([field], message)) + } + + case 'credentials_identifier_missing': + default: { + const field = resolveRequiredFieldName(credentials, ['email', 'username', 'phone', 'password']) + return createAuthFailurePayload(error.code, error.message, 422, createFieldErrors([field], error.message)) + } + } +} + +function createRegistrationFailure( + error: AuthError, + input: TInput, +): AuthFailure, readonly string[]>>> { + switch (error.code) { + case 'registration_identifier_taken': { + const field = resolveIdentifierFieldName(input, error) ?? resolveRequiredFieldName(input, ['email', 'username', 'phone', 'password']) + return createAuthFailurePayload(error.code, error.message, 422, createFieldErrors([field], error.message)) + } + + case 'password_confirmation_mismatch': { + const message = error.message + const fields = [ + pickInputField(input, ['password']), + pickInputField(input, ['passwordConfirmation']), + ].filter((field): field is InputFieldName => typeof field === 'string') + + return createAuthFailurePayload( + error.code, + message, + 422, + createFieldErrors(fields.length > 0 ? fields : [resolveRequiredFieldName(input, ['password'])], message), + ) + } + + case 'credentials_identifier_missing': + default: { + const field = resolveRequiredFieldName(input, ['email', 'username', 'phone', 'password']) + return createAuthFailurePayload(error.code, error.message, 422, createFieldErrors([field], error.message)) + } + } +} + +function createEmailVerificationResendFailure( + error: AuthError, +): AuthFailure> { + switch (error.code) { + case 'email_already_verified': + return createAuthFailurePayload(error.code, error.message, 409, createFieldErrors(['_root'], error.message)) + case 'email_verification_user_missing': + default: + return createAuthFailurePayload(error.code, error.message, 401, createFieldErrors(['_root'], error.message)) + } +} + +function createPasswordResetRequestFailure( + error: AuthError, + input: TInput, +): AuthFailure, readonly string[]>>> { + const field = resolveRequiredFieldName(input, ['email']) + return createAuthFailurePayload(error.code, error.message, 422, createFieldErrors([field], error.message)) +} + +function createPasswordResetConsumeFailure( + error: AuthError, + input: TInput, +): AuthFailure, readonly string[]>>> { + switch (error.code) { + case 'password_confirmation_mismatch': { + const message = error.message + const fields = [ + pickInputField(input, ['password']), + pickInputField(input, ['passwordConfirmation']), + ].filter((field): field is InputFieldName => typeof field === 'string') + + return createAuthFailurePayload( + error.code, + message, + 422, + createFieldErrors(fields.length > 0 ? fields : [resolveRequiredFieldName(input, ['password'])], message), + ) + } + + case 'provider_update_unsupported': { + const field = resolveRequiredFieldName(input, ['token']) + return createAuthFailurePayload(error.code, error.message, 500, createFieldErrors([field], error.message)) + } + + case 'password_reset_token_invalid': { + const field = resolveRequiredFieldName(input, ['token']) + return createAuthFailurePayload(error.code, error.message, 422, createFieldErrors([field], error.message)) + } + + case 'password_reset_token_expired': { + const message = 'This password reset link is invalid or has expired.' + const field = resolveRequiredFieldName(input, ['token']) + return createAuthFailurePayload(error.code, message, 422, createFieldErrors([field], message)) + } + + case 'password_reset_user_missing': + case 'auth_user_missing': + default: { + const field = resolveRequiredFieldName(input, ['token']) + return createAuthFailurePayload(error.code, error.message, 422, createFieldErrors([field], error.message)) + } + } +} + +function createEmailVerificationConsumeFailure( + error: AuthError, +): AuthFailure> { + switch (error.code) { + case 'provider_update_unsupported': + return createAuthFailurePayload(error.code, error.message, 500, createFieldErrors(['token'], error.message)) + + case 'email_verification_token_invalid': + return createAuthFailurePayload(error.code, error.message, 422, createFieldErrors(['token'], error.message)) + + case 'email_verification_token_expired': { + const message = 'This verification link is invalid or has expired.' + return createAuthFailurePayload(error.code, message, 422, createFieldErrors(['token'], message)) + } + + case 'auth_user_missing': + default: + return createAuthFailurePayload(error.code, error.message, 422, createFieldErrors(['token'], error.message)) + } +} + function getAuthRuntimeState(): { bindings?: RuntimeBindings sharedPasswordResetThrottleFailures?: Set @@ -191,7 +516,7 @@ async function loadOptionalSecurityModule(): Promise { + return await bindings.context.getRequestCookie?.(name) +} + +async function resolveRequestHeader( + bindings: RuntimeBindings, + name: string, +): Promise { + const value = await bindings.context.getRequestHeader?.(name) + return typeof value === 'string' && value.length > 0 ? value : undefined +} + +function parseBearerToken(header: string | undefined): string | undefined { + if (typeof header !== 'string') { + return undefined + } + + const match = header.match(/^Bearer\s+(.+)$/i) + return match?.[1]?.trim() || undefined +} + +async function hydrateGuardContextFromRequest(guardName: string): Promise { + const bindings = getRuntimeBindings() + const guard = getGuardConfig(guardName) + + if (guard.driver === 'token') { + if (!bindings.context.getAccessToken?.(guardName)) { + const accessToken = parseBearerToken(await resolveRequestHeader(bindings, 'authorization')) + if (accessToken) { + bindings.context.setAccessToken?.(guardName, accessToken) + } + } + + return + } + + if (!bindings.context.getSessionId(guardName)) { + const sessionCookie = parseSetCookieDefinition(bindings.session.sessionCookie('')) + if (sessionCookie) { + const sessionId = await resolveRequestCookie(bindings, sessionCookie.name) + if (sessionId) { + bindings.context.setSessionId(guardName, sessionId) + } + } + } + + if (!bindings.context.getRememberToken?.(guardName)) { + const rememberCookie = parseSetCookieDefinition(bindings.session.rememberMeCookie('')) + if (rememberCookie) { + const rememberToken = await resolveRequestCookie(bindings, rememberCookie.name) + if (rememberToken) { + bindings.context.setRememberToken?.(guardName, rememberToken) + } + } + } +} + function createPersonalAccessTokenId(): string { return randomUUID() } @@ -598,7 +983,7 @@ function createDefaultDeliveryHook(): AuthDeliveryHook { function ensureTokenStore(): AuthTokenStore { const bindings = getRuntimeBindings() if (!bindings.tokens) { - throw new Error('[@holo-js/auth] Personal access token runtime is not configured yet.') + throwAuthError('token_runtime_unconfigured', 'Personal access token runtime is not configured yet.') } return bindings.tokens @@ -607,7 +992,7 @@ function ensureTokenStore(): AuthTokenStore { function ensureEmailVerificationTokenStore(): EmailVerificationTokenStore { const bindings = getRuntimeBindings() if (!bindings.emailVerificationTokens) { - throw new Error('[@holo-js/auth] Email verification token runtime is not configured yet.') + throwAuthError('email_verification_runtime_unconfigured', 'Email verification token runtime is not configured yet.') } return bindings.emailVerificationTokens @@ -616,7 +1001,7 @@ function ensureEmailVerificationTokenStore(): EmailVerificationTokenStore { function ensurePasswordResetTokenStore(): PasswordResetTokenStore { const bindings = getRuntimeBindings() if (!bindings.passwordResetTokens) { - throw new Error('[@holo-js/auth] Password reset token runtime is not configured yet.') + throwAuthError('password_reset_runtime_unconfigured', 'Password reset token runtime is not configured yet.') } return bindings.passwordResetTokens @@ -723,7 +1108,9 @@ function getGuardConfig(guardName: string): RuntimeBindings['config']['guards'][ const bindings = getRuntimeBindings() const guard = bindings.config.guards[guardName] if (!guard) { - throw new Error(`[@holo-js/auth] Auth guard "${guardName}" is not configured.`) + throwAuthError('guard_not_configured', `Auth guard "${guardName}" is not configured.`, { + guard: guardName, + }) } return guard @@ -738,12 +1125,16 @@ function getProviderAdapter( const bindings = getRuntimeBindings() const providerConfig = bindings.config.providers[providerName] if (!providerConfig) { - throw new Error(`[@holo-js/auth] Auth provider "${providerName}" is not configured.`) + throwAuthError('provider_not_configured', `Auth provider "${providerName}" is not configured.`, { + provider: providerName, + }) } const adapter = bindings.providers[providerName] if (!adapter) { - throw new Error(`[@holo-js/auth] Auth provider runtime "${providerName}" is not configured.`) + throwAuthError('provider_runtime_not_configured', `Auth provider runtime "${providerName}" is not configured.`, { + provider: providerName, + }) } return { @@ -778,7 +1169,10 @@ function getGuardProviderAdapter( } { const guard = getGuardConfig(guardName) if (guard.driver !== 'session') { - throw new Error(`[@holo-js/auth] Auth guard "${guardName}" does not support session login.`) + throwAuthError('guard_session_login_unsupported', `Auth guard "${guardName}" does not support session login.`, { + guard: guardName, + driver: guard.driver, + }) } const provider = guard.provider @@ -793,7 +1187,7 @@ function getGuardProviderAdapter( function ensurePasswordConfirmation(input: AuthRegistrationInput): void { if (input.password !== input.passwordConfirmation) { - throw new Error('[@holo-js/auth] Password confirmation does not match.') + throwAuthError('password_confirmation_mismatch', 'Password confirmation does not match.') } } @@ -816,8 +1210,12 @@ function toLookupCredentials( ) if (Object.keys(credentials).length === 0) { - throw new Error( - `[@holo-js/auth] Auth credentials must include at least one configured identifier field: ${identifiers.join(', ')}.`, + throwAuthError( + 'credentials_identifier_missing', + `Auth credentials must include at least one configured identifier field: ${identifiers.join(', ')}.`, + { + identifiers, + }, ) } @@ -906,17 +1304,6 @@ function rehydrateSerializedUser( return Object.freeze(restored) } -function getEmailVerifiedAt( - adapter: ErasedAuthProviderAdapter, - user: unknown, -): Date | string | null | undefined { - if (adapter.getEmailVerifiedAt) { - return adapter.getEmailVerifiedAt(user) - } - - return requireRecordValue(user, '[@holo-js/auth] Auth provider users must be objects.').email_verified_at as Date | string | null | undefined -} - function getPasswordHash( adapter: ErasedAuthProviderAdapter, user: unknown, @@ -933,6 +1320,50 @@ function isEmailVerificationRequired(): boolean { return getRuntimeBindings().config.emailVerification.required === true } +function getDefaultGuardName(): string { + return getRuntimeBindings().config.defaults.guard +} + +function getEmailVerificationRoute(): string { + return getRuntimeBindings().config.emailVerification.route +} + +function getPasswordBrokerConfig(brokerName: string) { + const broker = getRuntimeBindings().config.passwords[brokerName] + if (!broker) { + throwAuthError('password_broker_not_configured', `Password broker "${brokerName}" is not configured.`, { + broker: brokerName, + }) + } + + return broker +} + +function getPasswordResetRoute(brokerName: string): string { + return getPasswordBrokerConfig(brokerName).route +} + +function hasVerifiedEmail(user: Readonly>): boolean { + return user.email_verified_at instanceof Date + || typeof user.email_verified_at === 'string' +} + +function createEmailVerificationRedirectRoute(user: AuthUser): string { + const route = getEmailVerificationRoute() + const rawEmail = (user as Readonly>).email + const email = typeof rawEmail === 'string' + ? rawEmail.trim() + : '' + + if (!email) { + return route + } + + const url = new URL(route, 'http://holo.local') + url.searchParams.set('email', email) + return `${url.pathname}${url.search}${url.hash}` +} + function toSessionIdentityPayload( guard: string, provider: string, @@ -1189,6 +1620,8 @@ async function resolveCurrentAccessTokenForGuard(guardName: string): Promise>) + ? { + emailVerificationRequired: true, + emailVerificationRoute: createEmailVerificationRedirectRoute(user), + } + : {}), }) } @@ -1883,7 +2375,10 @@ async function updateUserRecord( const { adapter } = getProviderAdapter(providerName) const user = await adapter.findById(userId) if (!user) { - throw new Error(`[@holo-js/auth] Auth user "${providerName}:${String(userId)}" no longer exists.`) + throwAuthError('auth_user_missing', `Auth user "${providerName}:${String(userId)}" no longer exists.`, { + provider: providerName, + userId, + }) } let updated: unknown = user @@ -1896,8 +2391,12 @@ async function updateUserRecord( || typeof input.email_verified_at !== 'undefined' || typeof input.password !== 'undefined' ) { - throw new Error( - `[@holo-js/auth] Auth provider "${providerName}" must implement update() to persist user changes.`, + throwAuthError( + 'provider_update_unsupported', + `Auth provider "${providerName}" must implement update() to persist user changes.`, + { + provider: providerName, + }, ) } @@ -1918,7 +2417,7 @@ function createEmailVerificationFacade(): AuthEmailVerificationFacade { ) const email = typeof serialized.email === 'string' ? serialized.email.trim() : '' if (!email) { - throw new Error('[@holo-js/auth] Email verification requires a user with an email address.') + throwAuthError('email_required_for_verification', 'Email verification requires a user with an email address.') } const store = ensureEmailVerificationTokenStore() @@ -1942,166 +2441,212 @@ function createEmailVerificationFacade(): AuthEmailVerificationFacade { user: serialized, email, token: result, + route: getEmailVerificationRoute(), }) return result }, - async consume(plainTextToken: string): Promise { - const parsed = parsePlainTextToken(plainTextToken) - if (!parsed) { - throw new Error('[@holo-js/auth] Invalid email verification token.') - } + resend(options: { readonly guard?: string, readonly expiresAt?: Date, readonly email?: string } = {}): Promise>> { + return captureExpectedAuthResult(async () => { + const guardName = options.guard ?? getDefaultGuardName() + let currentUser: AuthUser | null + if (typeof options.email === 'string' && options.email.trim().length > 0) { + const { provider, adapter } = getGuardProviderAdapter(guardName) + const matchedUser = await adapter.findByCredentials({ + email: options.email.trim(), + }) + currentUser = matchedUser + ? serializeUser(adapter, matchedUser, provider) + : null + } else { + currentUser = await resolveUserFromGuard(guardName, { fresh: true }) + } - const store = ensureEmailVerificationTokenStore() - const record = await store.findById(parsed.id) - if (!record || !verifyTokenSecret(parsed.secret, record.tokenHash) || record.expiresAt.getTime() <= Date.now()) { - throw new Error('[@holo-js/auth] Invalid or expired email verification token.') - } + if (!currentUser) { + throwAuthError('email_verification_user_missing', 'Sign in before requesting another verification email.', { + guard: guardName, + }) + } - const updated = await updateUserRecord(record.provider, record.userId, { - email_verified_at: new Date(), - }) - await store.delete(record.id) - return updated + if (hasVerifiedEmail(currentUser as Readonly>)) { + throwAuthError('email_already_verified', 'Your email address is already verified.', { + guard: guardName, + }) + } + + return await this.create(currentUser, { + guard: guardName, + expiresAt: options.expiresAt, + }) + }, EXPECTED_EMAIL_VERIFICATION_RESEND_ERRORS, createEmailVerificationResendFailure) + }, + consume(plainTextToken: string): Promise>> { + return captureExpectedAuthResult(async () => { + const parsed = parsePlainTextToken(plainTextToken) + if (!parsed) { + throwAuthError('email_verification_token_invalid', 'Invalid email verification token.') + } + + const store = ensureEmailVerificationTokenStore() + const record = await store.findById(parsed.id) + if (!record || !verifyTokenSecret(parsed.secret, record.tokenHash) || record.expiresAt.getTime() <= Date.now()) { + throwAuthError('email_verification_token_expired', 'Invalid or expired email verification token.') + } + + const updated = await updateUserRecord(record.provider, record.userId, { + email_verified_at: new Date(), + }) + await store.delete(record.id) + return updated + }, EXPECTED_EMAIL_VERIFICATION_CONSUME_ERRORS, createEmailVerificationConsumeFailure) }, }) } -function createPasswordResetFacade(): AuthPasswordResetFacade { - return Object.freeze({ - async request(email: string, options: { readonly broker?: string, readonly expiresAt?: Date } = {}): Promise { - const normalizedEmail = email.trim() - if (!normalizedEmail) { - throw new Error('[@holo-js/auth] Email is required to request a password reset.') - } +async function requestPasswordResetUsingRuntime( + input: TInput, + options: AuthPasswordResetRequestOptions = {}, +): Promise, readonly string[]>>>> { + return captureExpectedAuthResult(async () => { + const normalizedEmail = input.email.trim() + if (!normalizedEmail) { + throwAuthError('password_reset_email_required', 'Email is required to request a password reset.') + } - const bindings = getRuntimeBindings() - const brokerName = options.broker ?? bindings.config.defaults.passwords - const broker = bindings.config.passwords[brokerName] - if (!broker) { - throw new Error(`[@holo-js/auth] Password broker "${brokerName}" is not configured.`) + const bindings = getRuntimeBindings() + const brokerName = options.broker ?? bindings.config.defaults.passwords + const broker = bindings.config.passwords[brokerName] + if (!broker) { + throwAuthError('password_broker_not_configured', `Password broker "${brokerName}" is not configured.`, { + broker: brokerName, + }) + } + + const store = ensurePasswordResetTokenStore() + const existing = await store.findLatestByEmail(broker.provider, normalizedEmail, { + table: broker.table, + }) + if (existing && (existing.createdAt.getTime() + (broker.throttle * 60 * 1000)) > Date.now()) { + return + } + + let sharedReservation: { + readonly key: string + readonly limited: boolean + readonly store: OptionalSecurityRateLimitStore + readonly bypassed: boolean + } | undefined + + try { + sharedReservation = await reserveSharedPasswordResetThrottle(brokerName, broker, normalizedEmail) + if (sharedReservation?.limited) { + return } - const store = ensurePasswordResetTokenStore() - const existing = await store.findLatestByEmail(broker.provider, normalizedEmail, { - table: broker.table, + const { adapter } = getProviderAdapter(broker.provider) + const user = await adapter.findByCredentials({ + email: normalizedEmail, }) - if (existing && (existing.createdAt.getTime() + (broker.throttle * 60 * 1000)) > Date.now()) { + if (!user) { + await clearSharedPasswordResetThrottleReservation(sharedReservation) return } - let sharedReservation: { - readonly key: string - readonly limited: boolean - readonly store: OptionalSecurityRateLimitStore - readonly bypassed: boolean - } | undefined - + const id = createPersonalAccessTokenId() + const secret = createPersonalAccessTokenSecret() + const record: PasswordResetTokenRecord = Object.freeze({ + id, + provider: broker.provider, + email: normalizedEmail, + table: broker.table, + tokenHash: hashTokenSecret(secret), + createdAt: new Date(), + expiresAt: options.expiresAt ?? new Date(Date.now() + broker.expire * 60 * 1000), + }) + await store.deleteByEmail(broker.provider, normalizedEmail, { + table: broker.table, + }) + await store.create(record) + const result = createLifecycleTokenResult(record, `${id}.${secret}`) try { - sharedReservation = await reserveSharedPasswordResetThrottle(brokerName, broker, normalizedEmail) - if (sharedReservation?.limited) { - return - } - - const { adapter } = getProviderAdapter(broker.provider) - const user = await adapter.findByCredentials({ - email: normalizedEmail, - }) - if (!user) { - await clearSharedPasswordResetThrottleReservation(sharedReservation) - return - } - - const id = createPersonalAccessTokenId() - const secret = createPersonalAccessTokenSecret() - const record: PasswordResetTokenRecord = Object.freeze({ - id, + await bindings.delivery.sendPasswordReset({ + broker: brokerName, provider: broker.provider, email: normalizedEmail, - table: broker.table, - tokenHash: hashTokenSecret(secret), - createdAt: new Date(), - expiresAt: options.expiresAt ?? new Date(Date.now() + broker.expire * 60 * 1000), + token: result, + route: getPasswordResetRoute(brokerName), }) - await store.deleteByEmail(broker.provider, normalizedEmail, { - table: broker.table, - }) - await store.create(record) - const result = createLifecycleTokenResult(record, `${id}.${secret}`) + } catch (error) { try { - await bindings.delivery.sendPasswordReset({ - provider: broker.provider, - email: normalizedEmail, - token: result, + await store.delete(record.id, { + table: broker.table, }) - } catch (error) { - try { - await store.delete(record.id, { - table: broker.table, - }) - } catch (cleanupError) { - void cleanupError - } - throw error + } catch (cleanupError) { + void cleanupError } - if (sharedReservation?.bypassed) { - getAuthRuntimeState().sharedPasswordResetThrottleFailures?.delete(sharedReservation.key) - } - } catch (error) { - const cleared = await clearSharedPasswordResetThrottleReservation(sharedReservation) - if (cleared === 'unsupported' && sharedReservation) { - const failures = getAuthRuntimeState().sharedPasswordResetThrottleFailures ??= new Set() - failures.add(sharedReservation.key) - } - throw error } - }, - async consume(input: { - readonly token: string - readonly password: string - readonly passwordConfirmation: string - }): Promise { - if (input.password !== input.passwordConfirmation) { - throw new Error('[@holo-js/auth] Password confirmation does not match.') + if (sharedReservation?.bypassed) { + getAuthRuntimeState().sharedPasswordResetThrottleFailures?.delete(sharedReservation.key) } - - const parsed = parsePlainTextToken(input.token) - if (!parsed) { - throw new Error('[@holo-js/auth] Invalid password reset token.') + } catch (error) { + const cleared = await clearSharedPasswordResetThrottleReservation(sharedReservation) + if (cleared === 'unsupported' && sharedReservation) { + const failures = getAuthRuntimeState().sharedPasswordResetThrottleFailures ??= new Set() + failures.add(sharedReservation.key) } - const store = ensurePasswordResetTokenStore() - const record = await store.findById(parsed.id) - if (!record || !verifyTokenSecret(parsed.secret, record.tokenHash) || record.expiresAt.getTime() <= Date.now()) { - throw new Error('[@holo-js/auth] Invalid or expired password reset token.') - } + throw error + } + }, EXPECTED_PASSWORD_RESET_REQUEST_ERRORS, error => createPasswordResetRequestFailure(error, input)) +} - const { adapter } = getProviderAdapter(record.provider) - const user = await adapter.findByCredentials({ +async function resetPasswordUsingRuntime( + input: TInput, +): Promise, readonly string[]>>>> { + return captureExpectedAuthResult(async () => { + if (input.password !== input.passwordConfirmation) { + throwAuthError('password_confirmation_mismatch', 'Password confirmation does not match.') + } + + const parsed = parsePlainTextToken(input.token) + if (!parsed) { + throwAuthError('password_reset_token_invalid', 'Invalid password reset token.') + } + + const store = ensurePasswordResetTokenStore() + const record = await store.findById(parsed.id) + if (!record || !verifyTokenSecret(parsed.secret, record.tokenHash) || record.expiresAt.getTime() <= Date.now()) { + throwAuthError('password_reset_token_expired', 'Invalid or expired password reset token.') + } + + const { adapter } = getProviderAdapter(record.provider) + const user = await adapter.findByCredentials({ + email: record.email, + }) + if (!user) { + throwAuthError('password_reset_user_missing', 'Password reset token user no longer exists.', { + provider: record.provider, email: record.email, }) - if (!user) { - throw new Error('[@holo-js/auth] Password reset token user no longer exists.') - } + } - const password = await getRuntimeBindings().passwordHasher.hash(input.password) - const userId = requireUserId( - adapter, - user, - '[@holo-js/auth] Password reset token user is invalid.', - ) - const updated = await updateUserRecord(record.provider, userId, { - password, - }) - await store.delete(record.id, { - table: record.table, - }) - await store.deleteByEmail(record.provider, record.email, { - table: record.table, - }) - return updated - }, - }) + const password = await getRuntimeBindings().passwordHasher.hash(input.password) + const userId = requireUserId( + adapter, + user, + '[@holo-js/auth] Password reset token user is invalid.', + ) + const updated = await updateUserRecord(record.provider, userId, { + password, + }) + await store.delete(record.id, { + table: record.table, + }) + await store.deleteByEmail(record.provider, record.email, { + table: record.table, + }) + return updated + }, EXPECTED_PASSWORD_RESET_CONSUME_ERRORS, error => createPasswordResetConsumeFailure(error, input)) } function createTokenFacade(): AuthTokenFacade { @@ -2195,8 +2740,12 @@ function createGuardFacade(guardName: string): AuthGuardFacade { currentAccessToken() { return resolveCurrentAccessTokenForGuard(guardName) }, - login(credentials: AuthCredentials) { - return loginForGuard(guardName, credentials) + login(credentials: TCredentials) { + return captureExpectedAuthResult( + () => loginForGuard(guardName, credentials), + EXPECTED_LOGIN_ERRORS, + error => createLoginFailure(error, credentials), + ) }, loginUsing(user: unknown, options?: AuthSessionLoginOptions) { return loginUsingForGuard(guardName, user, options) @@ -2245,7 +2794,6 @@ export function getAuthRuntime(): AuthRuntimeFacade { const getDefaultGuardName = () => getRuntimeBindings().config.defaults.guard const tokens = createTokenFacade() const verification = createEmailVerificationFacade() - const passwords = createPasswordResetFacade() const facade: AuthFacade = { check() { @@ -2263,8 +2811,12 @@ export function getAuthRuntime(): AuthRuntimeFacade { currentAccessToken() { return resolveCurrentAccessTokenForGuard(getDefaultGuardName()) }, - login(credentials) { - return loginForGuard(getDefaultGuardName(), credentials) + login(credentials: TCredentials) { + return captureExpectedAuthResult( + () => loginForGuard(getDefaultGuardName(), credentials), + EXPECTED_LOGIN_ERRORS, + error => createLoginFailure(error, credentials), + ) }, loginUsing(user, options) { return loginUsingForGuard(getDefaultGuardName(), user, options) @@ -2287,8 +2839,18 @@ export function getAuthRuntime(): AuthRuntimeFacade { logout() { return logoutForGuard(getDefaultGuardName()) }, - register(input) { - return registerDefaultUser(input) + register(input: TInput) { + return captureExpectedAuthResult( + () => registerDefaultUser(input), + EXPECTED_REGISTRATION_ERRORS, + error => createRegistrationFailure(error, input), + ) + }, + requestPasswordReset(input: TInput, options?: AuthPasswordResetRequestOptions) { + return requestPasswordResetUsingRuntime(input, options) + }, + resetPassword(input: TInput) { + return resetPasswordUsingRuntime(input) }, hashPassword(password: string) { return getRuntimeBindings().passwordHasher.hash(password) @@ -2304,7 +2866,6 @@ export function getAuthRuntime(): AuthRuntimeFacade { }, tokens, verification, - passwords, } return Object.freeze({ @@ -2365,7 +2926,9 @@ export async function currentAccessToken(): Promise { +export async function login( + credentials: TCredentials, +): Promise, readonly string[]>>>> { return getAuthRuntime().login(credentials) } @@ -2421,10 +2984,25 @@ export async function logout(): Promise { return getAuthRuntime().logout() } -export async function register(input: AuthRegistrationInput): Promise { +export async function register( + input: TInput, +): Promise, readonly string[]>>>> { return getAuthRuntime().register(input) } +export async function requestPasswordReset( + input: TInput, + options?: AuthPasswordResetRequestOptions, +): Promise, readonly string[]>>>> { + return getAuthRuntime().requestPasswordReset(input, options) +} + +export async function resetPassword( + input: TInput, +): Promise, readonly string[]>>>> { + return getAuthRuntime().resetPassword(input) +} + export const tokens: AuthTokenFacade = Object.freeze({ create(user: unknown, options: PersonalAccessTokenCreationOptions) { return getAuthRuntime().tokens.create(user, options) @@ -2450,24 +3028,14 @@ export const verification: AuthEmailVerificationFacade = Object.freeze({ create(user: unknown, options?: { readonly guard?: string, readonly expiresAt?: Date }) { return getAuthRuntime().verification.create(user, options) }, + resend(options?: { readonly guard?: string, readonly expiresAt?: Date, readonly email?: string }) { + return getAuthRuntime().verification.resend(options) + }, consume(plainTextToken: string) { return getAuthRuntime().verification.consume(plainTextToken) }, }) -export const passwords: AuthPasswordResetFacade = Object.freeze({ - request(email: string, options?: { readonly broker?: string, readonly expiresAt?: Date }) { - return getAuthRuntime().passwords.request(email, options) - }, - consume(input: { - readonly token: string - readonly password: string - readonly passwordConfirmation: string - }) { - return getAuthRuntime().passwords.consume(input) - }, -}) - export const authRuntimeInternals = { createAsyncAuthContext, createDefaultPasswordHasher, diff --git a/packages/auth/tests/contracts.type.test.ts b/packages/auth/tests/contracts.type.test.ts index c2faa0c..01cfe91 100644 --- a/packages/auth/tests/contracts.type.test.ts +++ b/packages/auth/tests/contracts.type.test.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest' -import auth, { type getAuthRuntime, type AuthEstablishedSession, type AuthGuardFacade, type AuthImpersonationState, type AuthLogoutResult, type AuthProviderAdapter, type AuthRuntimeBindings, type AuthUser, type CurrentAuthResponse, type register, type user } from '../src' -import clientAuth, { type refreshUser as refreshClientUser, type user as clientUser } from '../src/client' +import auth, { AuthError, isAuthError, type AuthErrorCode, type AuthEstablishedSession, type AuthFailure, type AuthGuardFacade, type AuthImpersonationState, type AuthLoginErrorCode, type AuthLogoutResult, type AuthPasswordResetConsumeErrorCode, type AuthPasswordResetRequestErrorCode, type AuthProviderAdapter, type AuthRegistrationErrorCode, type AuthResult, type AuthRuntimeBindings, type AuthUser, type CurrentAuthResponse, type getAuthRuntime, type register, type user } from '../src' +import clientAuth, { type refreshUser as refreshClientUser, type useAuth as clientUseAuth, type user as clientUser } from '../src/client' declare module '../src' { interface HoloAuthTypeRegistry { @@ -27,6 +27,7 @@ describe('@holo-js/auth typing', () => { type RegisteredUser = Awaited> type CurrentServerUser = Awaited> type CurrentClientUser = Awaited> + type CurrentClientAuth = Awaited> type RefreshedClientUser = Awaited> type GuardUser = Awaited> type GuardRefreshedUser = Awaited> @@ -39,9 +40,13 @@ describe('@holo-js/auth typing', () => { type RuntimeLogoutAll = Awaited['logoutAll']>> expectTypeOf().toEqualTypeOf() - expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf>() expectTypeOf().toEqualTypeOf() expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf boolean + readonly refreshUser: () => Promise + }>() expectTypeOf().toEqualTypeOf() expectTypeOf().toEqualTypeOf() expectTypeOf().toEqualTypeOf() @@ -92,6 +97,7 @@ describe('@holo-js/auth typing', () => { expectTypeOf(adapter.serialize).returns.toEqualTypeOf() expectTypeOf(auth.user).returns.toEqualTypeOf>() + expectTypeOf(auth.login).returns.toEqualTypeOf>>() expectTypeOf(auth.loginUsing).returns.toEqualTypeOf>() expectTypeOf(auth.loginUsingId).returns.toEqualTypeOf>() expectTypeOf(auth.hashPassword).returns.toEqualTypeOf>() @@ -102,6 +108,37 @@ describe('@holo-js/auth typing', () => { expectTypeOf(auth.stopImpersonating).returns.toEqualTypeOf>() expectTypeOf(auth.logout).returns.toEqualTypeOf>() expectTypeOf(clientAuth.user).returns.toEqualTypeOf>() + expectTypeOf(clientAuth.useAuth).returns.toEqualTypeOf boolean + readonly refreshUser: () => Promise + }>>() + + async function requestPasswordResetResult() { + return await auth.requestPasswordReset({ + email: 'ava@example.com', + }) + } + + async function resetPasswordResult() { + return await auth.resetPassword({ + token: 'token-value', + password: 'secret-secret', + passwordConfirmation: 'secret-secret', + }) + } + + expectTypeOf>>().toEqualTypeOf< + AuthResult + >() + expectTypeOf>>().toEqualTypeOf< + AuthResult + >() }) it('keeps legacy custom session runtimes assignable to auth runtime bindings', () => { @@ -137,7 +174,7 @@ describe('@holo-js/auth typing', () => { password: 'secret-secret', } // @ts-expect-error passwordConfirmation must be required - const invalidPasswordResetInput: Parameters[0] = { + const invalidPasswordResetInput: Parameters[0] = { token: 'token-value', password: 'secret-secret', } @@ -145,4 +182,63 @@ describe('@holo-js/auth typing', () => { void invalidRegisterInput void invalidPasswordResetInput }) + + it('exposes a public auth error discriminator for catch-time narrowing', () => { + expectTypeOf().toEqualTypeOf< + | 'runtime_unconfigured' + | 'token_runtime_unconfigured' + | 'email_verification_runtime_unconfigured' + | 'password_reset_runtime_unconfigured' + | 'guard_not_configured' + | 'provider_not_configured' + | 'provider_runtime_not_configured' + | 'guard_session_login_unsupported' + | 'credentials_identifier_missing' + | 'password_confirmation_mismatch' + | 'invalid_credentials' + | 'email_verification_required' + | 'trusted_login_user_required' + | 'trusted_login_provider_mismatch' + | 'trusted_login_user_not_found' + | 'trusted_login_user_incompatible' + | 'impersonation_actor_required' + | 'impersonation_nested_unsupported' + | 'impersonation_already_active' + | 'registration_identifier_taken' + | 'auth_user_missing' + | 'provider_resolution_required' + | 'provider_update_unsupported' + | 'email_required_for_verification' + | 'email_verification_user_missing' + | 'email_already_verified' + | 'email_verification_token_invalid' + | 'email_verification_token_expired' + | 'password_reset_email_required' + | 'password_broker_not_configured' + | 'password_reset_token_invalid' + | 'password_reset_token_expired' + | 'password_reset_user_missing' + >() + + const error: unknown = null + if (isAuthError(error)) { + const code: AuthErrorCode = error.code + void code + } + }) + + it('exposes plain auth failure objects in result unions', () => { + expectTypeOf>().toMatchTypeOf<{ + code: AuthLoginErrorCode + message: string + status: number + fields: { + email?: readonly string[] + password?: readonly string[] + } + }>() + }) }) diff --git a/packages/auth/tests/docs-smoke.test.ts b/packages/auth/tests/docs-smoke.test.ts index 00422ec..f9ce034 100644 --- a/packages/auth/tests/docs-smoke.test.ts +++ b/packages/auth/tests/docs-smoke.test.ts @@ -77,20 +77,29 @@ describe('auth documentation smoke checks', () => { const reset = await readDoc('password-reset.md') expect(client).toContain('@holo-js/auth/client') + expect(client).toContain('useAuth') expect(client).toContain('refreshUser') expect(client).toContain('/api/auth/user') expect(client).toContain('check()') expect(client).toContain('does not expose') expect(client).toContain('hashPassword()') expect(client).toContain('impersonate()') - expect(verification).toContain('verification.create') + expect(verification).toContain('AUTH_EMAIL_VERIFICATION_ROUTE') + expect(verification).toContain('APP_URL') + expect(verification).toContain('emailVerificationRequired') + expect(verification).toContain('emailVerificationRoute') expect(verification).toContain('verification.consume') + expect(verification).toContain('verification.resend') expect(verification).toContain('@holo-js/notifications') - expect(verification).toContain('npx holo install mail') - expect(verification).toContain('notify(created, verificationCreated(token))') - expect(reset).toContain('passwords.request') - expect(reset).toContain('passwords.consume') - expect(reset).toContain('notifyUsing()') + expect(verification).toContain('@holo-js/mail') + expect(verification).not.toContain('notify(created, verificationCreated(token))') + expect(reset).toContain('requestPasswordReset') + expect(reset).toContain('resetPassword') + expect(reset).not.toContain('passwords.request') + expect(reset).not.toContain('passwords.consume') + expect(reset).toContain('AUTH_PASSWORD_RESET_ROUTE') + expect(reset).toContain('APP_URL') expect(reset).toContain('@holo-js/mail') + expect(reset).not.toContain('notifyUsing()') }) }) diff --git a/packages/auth/tests/package.test.ts b/packages/auth/tests/package.test.ts index 9ecc21a..ccfe408 100644 --- a/packages/auth/tests/package.test.ts +++ b/packages/auth/tests/package.test.ts @@ -15,14 +15,16 @@ import auth, { impersonate, impersonateById, impersonation, + isAuthError, login, loginUsing, loginUsingId, logout, needsPasswordRehash, - passwords, refreshUser, register, + requestPasswordReset, + resetPassword, resetAuthRuntime, stopImpersonating, tokens, @@ -36,6 +38,7 @@ import clientAuth, { configureAuthClient, refreshUser as clientRefreshUser, resetAuthClient, + useAuth as clientUseAuth, user as clientUser, } from '../src/client' @@ -49,8 +52,10 @@ function hashPasswordResetEmail(email: string, csrfSigningKey?: string): string return createHash('sha256').update(canonicalEmail).digest('hex') } import type { + AuthErrorCode, AuthDeliveryHook, AuthProviderAdapter, + AuthResult, HoloAuthConfig, AuthUser, EmailVerificationTokenRecord, @@ -61,6 +66,27 @@ import type { AuthTokenStore, } from '../src' +function unwrapAuthResult(result: AuthResult): TData { + if (result.error) { + throw new Error(`Expected auth success but received ${result.error.code}.`) + } + + return result.data +} + +function expectAuthFailureCode( + result: AuthResult, + code: TCode, +): NonNullable { + expect(result.error?.code).toBe(code) + if (!result.error) { + throw new Error(`Expected auth failure "${code}" but received success.`) + } + + expect(result.error.code).toBe(code) + return result.error +} + type UserRecord = { id: number name: string @@ -642,12 +668,12 @@ describe('@holo-js/auth package runtime', () => { expect(auth.currentAccessToken).toBe(currentAccessToken) expect(typeof auth.guard('web').check).toBe('function') - const created = await register({ + const created = unwrapAuthResult(await register({ name: 'Ava', email: 'ava@example.com', password: 'secret-secret', passwordConfirmation: 'secret-secret', - }) + })) expect(created).toMatchObject({ id: 1, @@ -660,20 +686,20 @@ describe('@holo-js/auth package runtime', () => { it('registers users, logs them in, resolves current user state, and logs them out', async () => { const runtime = configureRuntime() - const created = await auth.register({ + const created = unwrapAuthResult(await auth.register({ name: 'Ava', email: 'ava@example.com', password: 'secret-secret', passwordConfirmation: 'secret-secret', - }) + })) expect(created.id).toBe(1) expect(await check()).toBe(false) - const established = await login({ + const established = unwrapAuthResult(await login({ email: 'ava@example.com', password: 'secret-secret', remember: true, - }) as { + })) as { readonly guard?: string readonly sessionId?: string readonly rememberToken?: string @@ -725,12 +751,12 @@ describe('@holo-js/auth package runtime', () => { it('does not auto-start email verification during registration', async () => { const runtime = configureRuntime() - await expect(register({ + expect(unwrapAuthResult(await register({ name: 'Ava', email: 'ava@example.com', password: 'secret-secret', passwordConfirmation: 'secret-secret', - })).resolves.toMatchObject({ + }))).toMatchObject({ id: 1, email: 'ava@example.com', }) @@ -739,6 +765,29 @@ describe('@holo-js/auth package runtime', () => { expect(runtime.emailVerificationTokenStore.records.size).toBe(0) }) + it('auto-starts email verification during registration when verification is required', async () => { + const runtime = configureRuntime({ + emailVerificationRequired: true, + }) + + expect(unwrapAuthResult(await register({ + name: 'Ava', + email: 'ava@example.com', + password: 'secret-secret', + passwordConfirmation: 'secret-secret', + }))).toMatchObject({ + id: 1, + email: 'ava@example.com', + }) + + expect(runtime.deliveries).toHaveLength(1) + expect(runtime.deliveries[0]).toMatchObject({ + type: 'verification', + email: 'ava@example.com', + }) + expect(runtime.emailVerificationTokenStore.records.size).toBe(1) + }) + it('supports trusted session login with a user object or user id', async () => { const runtime = configureRuntime() @@ -1032,19 +1081,19 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date(), }) - const remembered = await login({ + const remembered = unwrapAuthResult(await login({ email: 'ava@example.com', password: 'secret-secret', remember: true, - }) + })) const rememberedRecord = runtime.sessionStore.records.get(remembered.sessionId) expect(rememberedRecord?.rememberTokenHash).toBeTypeOf('string') - const loggedInAgain = await login({ + const loggedInAgain = unwrapAuthResult(await login({ email: 'ava@example.com', password: 'secret-secret', - }) + })) const nextRecord = runtime.sessionStore.records.get(loggedInAgain.sessionId) expect(loggedInAgain.cookies).toHaveLength(1) @@ -1127,19 +1176,19 @@ describe('@holo-js/auth package runtime', () => { passwordConfirmation: 'secret-secret', }) - await expect(register({ + expectAuthFailureCode(await register({ name: 'Ava', email: 'ava@example.com', password: 'secret-secret', passwordConfirmation: 'secret-secret', - })).rejects.toThrow('already exists') + }), 'registration_identifier_taken') - await expect(register({ + expectAuthFailureCode(await register({ name: 'Mina', email: 'mina@example.com', password: 'secret-secret', passwordConfirmation: 'different-secret', - })).rejects.toThrow('Password confirmation does not match') + }), 'password_confirmation_mismatch') }) it('accepts non-email credentials when the application passes validated input', async () => { @@ -1157,11 +1206,11 @@ describe('@holo-js/auth package runtime', () => { }), }) - const created = await register({ + const created = unwrapAuthResult(await register({ phone: '45545454', password: 'secret-secret', passwordConfirmation: 'secret-secret', - }) + })) expect(created.id).toBe(1) @@ -1207,23 +1256,23 @@ describe('@holo-js/auth package runtime', () => { passwordConfirmation: 'secret-secret', }) - await expect(register({ + expectAuthFailureCode(await register({ name: 'Mina', email: 'ava@example.com', phone: '99999999', country: 'US', password: 'secret-secret', passwordConfirmation: 'secret-secret', - })).rejects.toThrow('email already exists') + }), 'registration_identifier_taken') - await expect(register({ + expectAuthFailureCode(await register({ name: 'Noor', email: 'noor@example.com', phone: '45545454', country: 'SA', password: 'secret-secret', passwordConfirmation: 'secret-secret', - })).rejects.toThrow('phone already exists') + }), 'registration_identifier_taken') await login({ email: 'ava@example.com', @@ -1266,20 +1315,20 @@ describe('@holo-js/auth package runtime', () => { }), }) - await expect(register({ + expectAuthFailureCode(await register({ name: 'Ava', country: 'EG', password: 'secret-secret', passwordConfirmation: 'secret-secret', - })).rejects.toThrow('configured identifier field: email, phone') + }), 'credentials_identifier_missing') - await expect(login({ + expectAuthFailureCode(await login({ country: 'EG', password: 'secret-secret', - })).rejects.toThrow('configured identifier field: email, phone') + }), 'credentials_identifier_missing') }) - it('rejects missing users, bad passwords, and unverified logins when verification is required', async () => { + it('rejects missing users, bad passwords, and allows unverified logins with verification routing when verification is required', async () => { const runtime = configureRuntime({ emailVerificationRequired: true, }) @@ -1292,26 +1341,36 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: null, }) - await expect(login({ + const missingCredentialsError = expectAuthFailureCode(await login({ email: 'missing@example.com', password: 'secret-secret', - })).rejects.toThrow('Invalid credentials') + }), 'invalid_credentials') + expect(missingCredentialsError.message).toBe('These credentials do not match our records.') + expect(missingCredentialsError.fields).toEqual({ + email: ['These credentials do not match our records.'], + password: ['These credentials do not match our records.'], + }) - await expect(login({ + expectAuthFailureCode(await login({ email: 'ava@example.com', password: 'bad-password', - })).rejects.toThrow('Invalid credentials') + }), 'invalid_credentials') - await expect(login({ + expect(unwrapAuthResult(await login({ email: 'ava@example.com', password: 'secret-secret', - })).rejects.toThrow('Email verification is required before login') + }))).toMatchObject({ + guard: 'web', + sessionId: expect.any(String), + emailVerificationRequired: true, + emailVerificationRoute: '/verify-email?email=ava%40example.com', + }) runtime.usersProvider.users.get(1)!.email_verified_at = new Date('2026-04-08T00:00:00.000Z') - await expect(login({ + expect(unwrapAuthResult(await login({ email: 'ava@example.com', password: 'secret-secret', - })).resolves.toMatchObject({ + }))).toMatchObject({ guard: 'web', sessionId: expect.any(String), }) @@ -1319,12 +1378,12 @@ describe('@holo-js/auth package runtime', () => { it('creates, expires, rejects, and consumes email verification tokens', async () => { const runtime = configureRuntime() - const created = await register({ + const created = unwrapAuthResult(await register({ name: 'Ava', email: 'ava@example.com', password: 'secret-secret', passwordConfirmation: 'secret-secret', - }) + })) const token = await verification.create(created) expect(token.plainTextToken).toContain('.') @@ -1336,10 +1395,18 @@ describe('@holo-js/auth package runtime', () => { }) expect(runtime.deliveries[0]?.tokenValue).toBe(token.plainTextToken) - await expect(verification.consume('bad-token')).rejects.toThrow('Invalid email verification token') - await expect(verification.consume(`${token.id}.wrong-secret`)).rejects.toThrow('Invalid or expired email verification token') + const invalidVerificationError = expectAuthFailureCode( + await verification.consume('bad-token'), + 'email_verification_token_invalid', + ) + expect(invalidVerificationError.message).toContain('Invalid email verification token') + + expectAuthFailureCode( + await verification.consume(`${token.id}.wrong-secret`), + 'email_verification_token_expired', + ) - const verified = await verification.consume(token.plainTextToken) + const verified = unwrapAuthResult(await verification.consume(token.plainTextToken)) expect(verified).toMatchObject({ id: 1, email: 'ava@example.com', @@ -1347,12 +1414,48 @@ describe('@holo-js/auth package runtime', () => { expect(runtime.usersProvider.users.get(1)?.email_verified_at).toBeInstanceOf(Date) expect(runtime.emailVerificationTokenStore.records.size).toBe(0) - await expect(verification.consume(token.plainTextToken)).rejects.toThrow('Invalid or expired email verification token') + expectAuthFailureCode(await verification.consume(token.plainTextToken), 'email_verification_token_expired') const expired = await verification.create(created, { expiresAt: new Date('2026-04-07T00:00:00.000Z'), }) - await expect(verification.consume(expired.plainTextToken)).rejects.toThrow('Invalid or expired email verification token') + expectAuthFailureCode(await verification.consume(expired.plainTextToken), 'email_verification_token_expired') + }) + + it('resends verification tokens by email without requiring an active session', async () => { + const runtime = configureRuntime({ + emailVerificationRequired: true, + }) + + await register({ + name: 'Ava', + email: 'ava@example.com', + password: 'secret-secret', + passwordConfirmation: 'secret-secret', + }) + + expect(runtime.deliveries).toHaveLength(1) + + const resent = unwrapAuthResult(await verification.resend({ + email: 'ava@example.com', + })) + expect(resent.plainTextToken).toContain('.') + expect(runtime.deliveries).toHaveLength(2) + expect(runtime.deliveries[1]).toMatchObject({ + type: 'verification', + email: 'ava@example.com', + tokenId: resent.id, + tokenValue: resent.plainTextToken, + }) + + expectAuthFailureCode(await verification.resend({ + email: 'missing@example.com', + }), 'email_verification_user_missing') + + await verification.consume(resent.plainTextToken) + expectAuthFailureCode(await verification.resend({ + email: 'ava@example.com', + }), 'email_already_verified') }) it('fails verification and password reset flows when a provider cannot persist user changes', async () => { @@ -1449,17 +1552,20 @@ describe('@holo-js/auth package runtime', () => { }) const verificationToken = await verification.create(created) - await expect(verification.consume(verificationToken.plainTextToken)).rejects.toThrow('must implement update()') + expectAuthFailureCode( + await verification.consume(verificationToken.plainTextToken), + 'provider_update_unsupported', + ) expect(usersProvider.users.get(1)?.email_verified_at).toBeNull() - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) const resetDelivery = deliveries.find(delivery => delivery.type === 'password-reset') expect(resetDelivery).toBeDefined() - await expect(passwords.consume({ + expectAuthFailureCode(await resetPassword({ token: resetDelivery!.tokenValue, password: 'new-secret', passwordConfirmation: 'new-secret', - })).rejects.toThrow('must implement update()') + }), 'provider_update_unsupported') await expect( authRuntimeInternals.createDefaultPasswordHasher().verify( 'secret-secret', @@ -1489,63 +1595,63 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(runtime.deliveries).toHaveLength(1) const firstDelivery = runtime.deliveries[0]! expect(firstDelivery.type).toBe('password-reset') - await expect(passwords.consume({ + expectAuthFailureCode(await resetPassword({ token: 'bad-token', password: 'new-secret', passwordConfirmation: 'new-secret', - })).rejects.toThrow('Invalid password reset token') + }), 'password_reset_token_invalid') - await expect(passwords.consume({ + expectAuthFailureCode(await resetPassword({ token: firstDelivery.tokenValue, password: 'new-secret', - } as never)).rejects.toThrow('Password confirmation does not match') + } as never), 'password_confirmation_mismatch') - await expect(passwords.consume({ + expectAuthFailureCode(await resetPassword({ token: firstDelivery.tokenValue, password: 'new-secret', passwordConfirmation: 'wrong-secret', - })).rejects.toThrow('Password confirmation does not match') + }), 'password_confirmation_mismatch') - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(runtime.deliveries).toHaveLength(2) - await expect(passwords.consume({ + expectAuthFailureCode(await resetPassword({ token: firstDelivery.tokenValue, password: 'new-secret', passwordConfirmation: 'new-secret', - })).rejects.toThrow('Invalid or expired password reset token') + }), 'password_reset_token_expired') const activeDelivery = runtime.deliveries[1]! - const resetUser = await passwords.consume({ + const resetUser = unwrapAuthResult(await resetPassword({ token: activeDelivery.tokenValue, password: 'new-secret', passwordConfirmation: 'new-secret', - }) + })) expect(resetUser).toMatchObject({ email: 'ava@example.com', }) expect(runtime.passwordResetTokenStore.records.size).toBe(0) - await expect(login({ + expect(unwrapAuthResult(await login({ email: 'ava@example.com', password: 'new-secret', - })).resolves.toMatchObject({ + }))).toMatchObject({ guard: 'web', sessionId: expect.any(String), }) - await passwords.request('ava@example.com', { + await requestPasswordReset({ email: 'ava@example.com' }, { expiresAt: new Date('2026-04-07T00:00:00.000Z'), }) const expiredDelivery = runtime.deliveries[2]! - await expect(passwords.consume({ + expectAuthFailureCode(await resetPassword({ token: expiredDelivery.tokenValue, password: 'another-secret', passwordConfirmation: 'another-secret', - })).rejects.toThrow('Invalid or expired password reset token') + }), 'password_reset_token_expired') }) it('refreshes the session idle timeout when resolving a session-backed user', async () => { @@ -1586,10 +1692,10 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - const established = await login({ + const established = unwrapAuthResult(await login({ email: 'ava@example.com', password: 'secret-secret', - }) + })) const initialRecord = runtime.sessionStore.records.get(established.sessionId) expect(initialRecord).toBeDefined() @@ -1609,10 +1715,10 @@ describe('@holo-js/auth package runtime', () => { it('rejects registration when password confirmation is omitted', async () => { configureRuntime() - await expect(register({ + expectAuthFailureCode(await register({ email: 'ava@example.com', password: 'secret-secret', - } as never)).rejects.toThrow('Password confirmation does not match') + } as never), 'password_confirmation_mismatch') }) it('keeps password reset tokens scoped to their configured broker table', async () => { @@ -1645,10 +1751,10 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('ava@example.com', { + await requestPasswordReset({ email: 'ava@example.com' }, { broker: 'users', }) - await passwords.request('ava@example.com', { + await requestPasswordReset({ email: 'ava@example.com' }, { broker: 'admins', }) @@ -1660,11 +1766,11 @@ describe('@holo-js/auth package runtime', () => { ]) const userBrokerToken = runtime.deliveries[0]!.tokenValue - await expect(passwords.consume({ + expect(unwrapAuthResult(await resetPassword({ token: userBrokerToken, password: 'new-secret', passwordConfirmation: 'new-secret', - })).resolves.toMatchObject({ + }))).toMatchObject({ email: 'ava@example.com', }) @@ -1694,12 +1800,12 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(runtime.deliveries).toHaveLength(1) const firstDelivery = runtime.deliveries[0]! const firstRecord = runtime.passwordResetTokenStore.records.get(firstDelivery.tokenId) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(runtime.deliveries).toHaveLength(1) expect(runtime.passwordResetTokenStore.records.size).toBe(1) @@ -1751,7 +1857,7 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(runtime.deliveries).toHaveLength(1) const firstDelivery = runtime.deliveries[0] @@ -1769,7 +1875,7 @@ describe('@holo-js/auth package runtime', () => { createdAt: new Date(Date.now() - (2 * 60 * 60 * 1000)), })) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(hit).toHaveBeenCalledTimes(2) expect(hit).toHaveBeenNthCalledWith(1, `auth:password-reset:users:users:password_reset_tokens:${hashPasswordResetEmail('ava@example.com')}`, { @@ -1817,7 +1923,7 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(hit).toHaveBeenCalledWith( `auth:password-reset:${createHash('sha256').update('signing-key').digest('hex').slice(0, 16)}:users:users:password_reset_tokens:${hashPasswordResetEmail('ava@example.com', 'signing-key')}`, @@ -1876,9 +1982,9 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await expect(passwords.request('ava@example.com')).rejects.toThrow('token persistence failed') + await expect(requestPasswordReset({ email: 'ava@example.com' })).rejects.toThrow('token persistence failed') - await expect(passwords.request('ava@example.com')).resolves.toBeUndefined() + expect(unwrapAuthResult(await requestPasswordReset({ email: 'ava@example.com' }))).toBeUndefined() expect(findByCredentials).toHaveBeenCalledTimes(2) expect(runtime.deliveries).toHaveLength(1) @@ -1911,12 +2017,12 @@ describe('@holo-js/auth package runtime', () => { throw new Error('delivery failed') }) - await expect(passwords.request('ava@example.com')).rejects.toThrow('delivery failed') + await expect(requestPasswordReset({ email: 'ava@example.com' })).rejects.toThrow('delivery failed') expect(runtime.passwordResetTokenStore.records.size).toBe(0) bindings.delivery.sendPasswordReset = originalSendPasswordReset - await expect(passwords.request('ava@example.com')).resolves.toBeUndefined() + expect(unwrapAuthResult(await requestPasswordReset({ email: 'ava@example.com' }))).toBeUndefined() expect(runtime.deliveries).toHaveLength(1) expect(runtime.passwordResetTokenStore.records.size).toBe(1) }) @@ -1949,7 +2055,7 @@ describe('@holo-js/auth package runtime', () => { throw new Error('delivery failed') }) - await expect(passwords.request('ava@example.com')).rejects.toThrow('delivery failed') + await expect(requestPasswordReset({ email: 'ava@example.com' })).rejects.toThrow('delivery failed') expect(deleteSpy).toHaveBeenCalledTimes(1) bindings.delivery.sendPasswordReset = originalSendPasswordReset @@ -2004,13 +2110,13 @@ describe('@holo-js/auth package runtime', () => { const findByCredentials = vi.spyOn(runtime.usersProvider, 'findByCredentials') findByCredentials.mockRejectedValueOnce(new Error('provider lookup failed')) - await expect(passwords.request('ava@example.com')).rejects.toThrow('provider lookup failed') + await expect(requestPasswordReset({ email: 'ava@example.com' })).rejects.toThrow('provider lookup failed') expect(hit).toHaveBeenCalledTimes(1) expect((globalThis as typeof globalThis & { __holoAuthRuntime__?: { sharedPasswordResetThrottleFailures?: Set } }).__holoAuthRuntime__?.sharedPasswordResetThrottleFailures?.size).toBe(0) - await expect(passwords.request('ava@example.com')).resolves.toBeUndefined() + expect(unwrapAuthResult(await requestPasswordReset({ email: 'ava@example.com' }))).toBeUndefined() expect(hit).toHaveBeenCalledTimes(2) expect(findByCredentials).toHaveBeenCalledTimes(1) @@ -2070,10 +2176,10 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('missing@example.com') + await requestPasswordReset({ email: 'missing@example.com' }) expect(runtime.deliveries).toHaveLength(0) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(runtime.deliveries).toHaveLength(1) expect(hit).toHaveBeenCalledTimes(2) @@ -2131,10 +2237,10 @@ describe('@holo-js/auth package runtime', () => { const findByCredentials = vi.spyOn(runtime.usersProvider, 'findByCredentials') findByCredentials.mockRejectedValueOnce(new Error('provider lookup failed')) - await expect(passwords.request('ava@example.com')).rejects.toThrow('provider lookup failed') + await expect(requestPasswordReset({ email: 'ava@example.com' })).rejects.toThrow('provider lookup failed') expect(hit).toHaveBeenCalledTimes(1) - await expect(passwords.request('ava@example.com')).resolves.toBeUndefined() + expect(unwrapAuthResult(await requestPasswordReset({ email: 'ava@example.com' }))).toBeUndefined() expect(hit).toHaveBeenCalledTimes(1) expect(findByCredentials).toHaveBeenCalledTimes(2) @@ -2178,7 +2284,7 @@ describe('@holo-js/auth package runtime', () => { }, }) - await passwords.request('missing@example.com') + await requestPasswordReset({ email: 'missing@example.com' }) expect(runtime.deliveries).toHaveLength(0) const password = await authRuntimeInternals.createDefaultPasswordHasher().hash('secret-secret') @@ -2189,7 +2295,7 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('missing@example.com') + await requestPasswordReset({ email: 'missing@example.com' }) expect(hit).toHaveBeenCalledTimes(2) expect(hit).toHaveBeenNthCalledWith(1, `auth:password-reset:users:users:password_reset_tokens:${hashPasswordResetEmail('missing@example.com')}`, { @@ -2247,7 +2353,7 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(runtime.deliveries).toHaveLength(0) expect(runtime.passwordResetTokenStore.records.size).toBe(0) @@ -2301,7 +2407,7 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) resetAuthRuntime() resetSessionRuntime() @@ -2326,7 +2432,7 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(hit).toHaveBeenCalledTimes(2) expect(hit).toHaveBeenNthCalledWith(1, expect.stringMatching(/^auth:password-reset:[0-9a-f]{16}:users:users:password_reset_tokens:[0-9a-f]{64}$/), { @@ -2383,7 +2489,7 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(findByCredentials).toHaveBeenCalledTimes(1) const firstDelivery = runtime.deliveries[0] @@ -2401,7 +2507,7 @@ describe('@holo-js/auth package runtime', () => { createdAt: new Date(Date.now() - (2 * 60 * 60 * 1000)), })) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(hit).toHaveBeenCalledTimes(2) expect(findByCredentials).toHaveBeenCalledTimes(1) @@ -2436,7 +2542,7 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(findByCredentials).toHaveBeenCalledTimes(1) expect(runtime.deliveries).toHaveLength(1) @@ -2471,7 +2577,7 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) const firstDelivery = runtime.deliveries[0] if (!firstDelivery) { throw new Error('Expected first password reset delivery.') @@ -2487,7 +2593,7 @@ describe('@holo-js/auth package runtime', () => { createdAt: new Date(Date.now() - (2 * 60 * 60 * 1000)), })) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(runtime.deliveries).toHaveLength(2) }) @@ -2529,7 +2635,7 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) expect(hit).toHaveBeenCalledTimes(1) expect(findByCredentials).not.toHaveBeenCalled() @@ -2550,19 +2656,19 @@ describe('@holo-js/auth package runtime', () => { throw 'boom' }) - await expect(passwords.request('ava@example.com')).rejects.toBe('boom') + await expect(requestPasswordReset({ email: 'ava@example.com' })).rejects.toBe('boom') vi.stubGlobal('__holoAuthSecurityImport__', async () => { throw new Error('security import exploded') }) - await expect(passwords.request('ava@example.com')).rejects.toThrow('security import exploded') + await expect(requestPasswordReset({ email: 'ava@example.com' })).rejects.toThrow('security import exploded') vi.stubGlobal('__holoAuthSecurityImport__', async () => { throw new Error('Could not resolve "@holo-js/other"') }) - await expect(passwords.request('ava@example.com')).rejects.toThrow('Could not resolve "@holo-js/other"') + await expect(requestPasswordReset({ email: 'ava@example.com' })).rejects.toThrow('Could not resolve "@holo-js/other"') }) it('treats resolver-style optional security import failures as missing packages during password reset throttling', async () => { @@ -2590,7 +2696,7 @@ describe('@holo-js/auth package runtime', () => { throw new Error('Could not resolve "@holo-js/security"') }) - await expect(passwords.request('ava@example.com')).resolves.toBeUndefined() + expect(unwrapAuthResult(await requestPasswordReset({ email: 'ava@example.com' }))).toBeUndefined() expect(runtime.deliveries).toHaveLength(1) }) @@ -2681,10 +2787,10 @@ describe('@holo-js/auth package runtime', () => { const findByCredentials = vi.spyOn(runtime.usersProvider, 'findByCredentials') findByCredentials.mockRejectedValueOnce(new Error('provider lookup failed')) - await expect(passwords.request('ava@example.com')).rejects.toThrow('provider lookup failed') + await expect(requestPasswordReset({ email: 'ava@example.com' })).rejects.toThrow('provider lookup failed') expect(clear).toHaveBeenCalledTimes(1) expect(attempts.size).toBe(0) - await expect(passwords.request('ava@example.com')).resolves.toBeUndefined() + expect(unwrapAuthResult(await requestPasswordReset({ email: 'ava@example.com' }))).toBeUndefined() expect(findByCredentials).toHaveBeenCalledTimes(2) expect(runtime.deliveries).toHaveLength(1) @@ -2886,10 +2992,10 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - const webSession = await auth.guard('web').login({ + const webSession = unwrapAuthResult(await auth.guard('web').login({ email: 'ava@example.com', password: 'secret-secret', - }) + })) const originalRecord = runtime.sessionStore.records.get(webSession.sessionId) expect(originalRecord).toBeTruthy() @@ -2908,10 +3014,10 @@ describe('@holo-js/auth package runtime', () => { runtime.context.setSessionId('admin', webSession.sessionId) - const adminSession = await auth.guard('admin').login({ + const adminSession = unwrapAuthResult(await auth.guard('admin').login({ email: 'admin@example.com', password: 'admin-secret', - }) + })) expect(adminSession.sessionId).toBe(webSession.sessionId) expect(runtime.sessionStore.records).toHaveLength(1) @@ -3044,17 +3150,17 @@ describe('@holo-js/auth package runtime', () => { email_verified_at: new Date('2026-04-08T00:00:00.000Z'), }) - const webSession = await auth.guard('web').login({ + const webSession = unwrapAuthResult(await auth.guard('web').login({ email: 'ava@example.com', password: 'secret-secret', - }) + })) runtime.context.setSessionId('admin', webSession.sessionId) - const adminSession = await auth.guard('admin').login({ + const adminSession = unwrapAuthResult(await auth.guard('admin').login({ email: 'admin@example.com', password: 'admin-secret', - }) + })) expect(adminSession.sessionId).toBe(webSession.sessionId) @@ -3629,6 +3735,7 @@ describe('@holo-js/auth package runtime', () => { it('supports default and named client exports', () => { expect(clientAuth.check).toBe(clientCheck) + expect(clientAuth.useAuth).toBe(clientUseAuth) expect(clientAuth.user).toBe(clientUser) expect(clientAuth.refreshUser).toBe(clientRefreshUser) }) @@ -3670,6 +3777,7 @@ describe('@holo-js/auth package runtime', () => { const first = await clientUser() const second = await clientUser() + const authState = await clientUseAuth() expect(first).toMatchObject({ name: 'Ava', hit: 1, @@ -3678,9 +3786,18 @@ describe('@holo-js/auth package runtime', () => { name: 'Ava', hit: 1, }) + expect(authState).toMatchObject({ + authenticated: true, + guard: 'web', + user: { + name: 'Ava', + hit: 1, + }, + }) + expect(authState.check()).toBe(true) expect(fetchMock).toHaveBeenCalledTimes(1) - const refreshed = await clientRefreshUser() + const refreshed = await authState.refreshUser() expect(refreshed).toMatchObject({ name: 'Ava', hit: 2, @@ -3876,15 +3993,15 @@ describe('@holo-js/auth package runtime', () => { }) const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const created = await register({ + const created = unwrapAuthResult(await register({ name: 'Ava', email: 'ava@example.com', password: 'secret-secret', passwordConfirmation: 'secret-secret', - }) + })) const verifyToken = await verification.create(created) - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) const resetRecord = [...passwordResetTokenStore.records.values()][0] expect(warn).toHaveBeenCalledTimes(2) @@ -4149,7 +4266,7 @@ describe('@holo-js/auth package runtime', () => { await expect(verification.create(created, { guard: 'web', })).rejects.toThrow('Email verification token runtime is not configured yet') - await expect(passwords.request('ava@example.com')).rejects.toThrow('Password reset token runtime is not configured yet') + await expect(requestPasswordReset({ email: 'ava@example.com' })).rejects.toThrow('Password reset token runtime is not configured yet') await expect(auth.guard('api').loginUsing(created)).rejects.toThrow('does not support session login') configureAuthRuntime({ @@ -4372,21 +4489,21 @@ describe('@holo-js/auth package runtime', () => { await expect(verification.create(noEmail, { guard: 'web', })).rejects.toThrow('Email verification requires a user with an email address.') - await expect(passwords.request(' ')).rejects.toThrow('Email is required to request a password reset.') - await expect(passwords.request('ava@example.com', { + expectAuthFailureCode(await requestPasswordReset({ email: ' ' }), 'password_reset_email_required') + await expect(requestPasswordReset({ email: 'ava@example.com' }, { broker: 'missing', })).rejects.toThrow('Password broker "missing" is not configured.') - await expect(passwords.request('missing@example.com')).resolves.toBeUndefined() + expect(unwrapAuthResult(await requestPasswordReset({ email: 'missing@example.com' }))).toBeUndefined() - await passwords.request('ava@example.com') + await requestPasswordReset({ email: 'ava@example.com' }) const resetDelivery = runtime.deliveries.find(entry => entry.type === 'password-reset') runtime.usersProvider.users.delete(created.id) runtime.usersProvider.usersByEmail.delete(created.email) - await expect(passwords.consume({ + expectAuthFailureCode(await resetPassword({ token: resetDelivery!.tokenValue, password: 'new-secret', passwordConfirmation: 'new-secret', - })).rejects.toThrow('Password reset token user no longer exists.') + }), 'password_reset_user_missing') const listedUser = await runtime.usersProvider.create({ name: 'List Me', @@ -4494,10 +4611,10 @@ describe('@holo-js/auth package runtime', () => { context: authRuntimeInternals.createMemoryAuthContext(), }) - await expect(login({ + expect(unwrapAuthResult(await login({ email: 'ava@example.com', password: 'secret-secret', - })).resolves.toMatchObject({ + }))).toMatchObject({ user: { id: 1, email: 'ava@example.com', @@ -4969,10 +5086,10 @@ describe('@holo-js/auth package runtime', () => { }, }) - await expect(login({ + expect(unwrapAuthResult(await login({ email: 'ava@example.com', password: 'secret-secret', - })).resolves.toMatchObject({ + }))).toMatchObject({ user: { email: 'ava@example.com', }, diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index c524f66..bd8fafc 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -611,9 +611,13 @@ export function createInternalCommands( const changed = result.updatedPackageJson || result.createdMailConfig || result.createdMailDirectory + || result.updatedEnv + || result.updatedEnvExample writeLine(context.stdout, changed ? 'Installed mail support.' : 'Mail support is already installed.') if (result.updatedPackageJson) writeLine(context.stdout, ' - updated package.json') + if (result.updatedEnv) writeLine(context.stdout, ' - updated .env') + if (result.updatedEnvExample) writeLine(context.stdout, ' - updated .env.example') if (result.createdMailConfig) writeLine(context.stdout, ' - created config/mail.ts') if (result.createdMailDirectory) writeLine(context.stdout, ' - created server/mail') return diff --git a/packages/cli/src/project/scaffold.ts b/packages/cli/src/project/scaffold.ts index db3b7ee..10075aa 100644 --- a/packages/cli/src/project/scaffold.ts +++ b/packages/cli/src/project/scaffold.ts @@ -97,6 +97,7 @@ import { renderAuthorizationPoliciesReadme, renderCacheEnvFiles, renderEnvFileContents, + renderMailEnvFiles, renderNotificationsMigration, renderQueueEnvFiles, renderScaffoldAppConfig, @@ -448,6 +449,8 @@ export async function installMailIntoProject( const mailConfigPath = await resolveFirstExistingPath(projectRoot, MAIL_CONFIG_FILE_NAMES) const mailRoot = resolve(projectRoot, 'server/mail') const mailDirectoryExists = await pathExists(mailRoot) + const envPath = resolve(projectRoot, '.env') + const envExamplePath = resolve(projectRoot, '.env.example') await mkdir(resolve(projectRoot, 'config'), { recursive: true }) await mkdir(mailRoot, { recursive: true }) @@ -456,10 +459,24 @@ export async function installMailIntoProject( await writeTextFile(resolve(projectRoot, 'config/mail.ts'), renderMailConfig()) } + const mailEnvFiles = renderMailEnvFiles() + const nextEnv = upsertEnvContents(await readTextFile(envPath), mailEnvFiles.env) + const nextEnvExample = upsertEnvContents(await readTextFile(envExamplePath), mailEnvFiles.example) + + if (nextEnv.changed && typeof nextEnv.contents === 'string') { + await writeTextFile(envPath, nextEnv.contents) + } + + if (nextEnvExample.changed && typeof nextEnvExample.contents === 'string') { + await writeTextFile(envExamplePath, nextEnvExample.contents) + } + return { updatedPackageJson: await upsertMailPackageDependency(projectRoot), createdMailConfig: !mailConfigPath, createdMailDirectory: !mailDirectoryExists, + updatedEnv: nextEnv.changed, + updatedEnvExample: nextEnvExample.changed, } } @@ -653,6 +670,7 @@ export { renderFrameworkFiles, renderFrameworkRunner, renderMailConfig, + renderMailEnvFiles, renderMediaConfig, renderNotificationsConfig, renderNotificationsMigration, diff --git a/packages/cli/src/project/scaffold/config-renderers.ts b/packages/cli/src/project/scaffold/config-renderers.ts index bbeb77a..fbf1a07 100644 --- a/packages/cli/src/project/scaffold/config-renderers.ts +++ b/packages/cli/src/project/scaffold/config-renderers.ts @@ -246,6 +246,7 @@ export function renderMailConfig(): string { ' },', ' log: {', ' driver: \'log\',', + ' logBodies: env(\'MAIL_LOG_BODIES\', false),', ' },', ' fake: {', ' driver: \'fake\',', @@ -255,6 +256,8 @@ export function renderMailConfig(): string { ' host: env(\'MAIL_HOST\', \'127.0.0.1\'),', ' port: env(\'MAIL_PORT\', 1025),', ' secure: env(\'MAIL_SECURE\', false),', + ' user: env(\'MAIL_USERNAME\') || undefined,', + ' password: env(\'MAIL_PASSWORD\') || undefined,', ' },', ' },', '})', @@ -733,10 +736,12 @@ export function renderAuthConfig( ' table: \'password_reset_tokens\',', ' expire: 60,', ' throttle: 60,', + ' route: \'/reset-password\',', ' },', ' },', ' emailVerification: {', ' required: false,', + ' route: \'/verify-email\',', ' },', ' personalAccessTokens: {', ' defaultAbilities: [],', diff --git a/packages/cli/src/project/scaffold/framework-renderers.ts b/packages/cli/src/project/scaffold/framework-renderers.ts index 44ff9e5..c77d037 100644 --- a/packages/cli/src/project/scaffold/framework-renderers.ts +++ b/packages/cli/src/project/scaffold/framework-renderers.ts @@ -248,19 +248,44 @@ function renderSvelteServerUserHooks(): string { ].join('\n') } -function renderSvelteViteConfig(storageEnabled: boolean): string { +function renderSvelteViteConfig(_storageEnabled: boolean): string { const externals = [ ' \'@holo-js/adapter-sveltekit\',', + ' \'@holo-js/auth\',', + ' \'@holo-js/auth-clerk\',', + ' \'@holo-js/auth-social\',', + ' \'@holo-js/auth-workos\',', + ' \'@holo-js/authorization\',', + ' \'@holo-js/broadcast\',', + ' \'@holo-js/cache\',', + ' \'@holo-js/cache-db\',', + ' \'@holo-js/cache-redis\',', ' \'@holo-js/config\',', ' \'@holo-js/core\',', ' \'@holo-js/db\',', - ...(storageEnabled - ? [ - ' \'@holo-js/storage\',', - ' \'@holo-js/storage/runtime\',', - ] - : []), + ' \'@holo-js/db-mysql\',', + ' \'@holo-js/db-postgres\',', + ' \'@holo-js/db-sqlite\',', + ' \'@holo-js/events\',', + ' \'@holo-js/flux\',', + ' \'@holo-js/flux-svelte\',', + ' \'@holo-js/forms\',', + ' \'@holo-js/mail\',', + ' \'@holo-js/media\',', + ' \'@holo-js/notifications\',', + ' \'@holo-js/queue\',', + ' \'@holo-js/queue-db\',', + ' \'@holo-js/queue-redis\',', + ' \'@holo-js/security\',', + ' \'@holo-js/session\',', + ' \'@holo-js/storage\',', + ' \'@holo-js/storage/runtime\',', + ' \'@holo-js/storage-s3\',', + ' \'@holo-js/validation\',', ' \'better-sqlite3\',', + ' \'ioredis\',', + ' \'mysql2\',', + ' \'pg\',', ] return [ diff --git a/packages/cli/src/project/scaffold/project-renderers.ts b/packages/cli/src/project/scaffold/project-renderers.ts index 53d1ec1..2413820 100644 --- a/packages/cli/src/project/scaffold/project-renderers.ts +++ b/packages/cli/src/project/scaffold/project-renderers.ts @@ -29,6 +29,8 @@ export function renderAuthEnvFiles( : [] const env = [ 'AUTH_SOCIAL_ENCRYPTION_KEY=', + 'AUTH_EMAIL_VERIFICATION_ROUTE=/verify-email', + 'AUTH_PASSWORD_RESET_ROUTE=/reset-password', 'SESSION_DRIVER=file', `SESSION_CONNECTION=${defaultDatabaseConnection}`, 'SESSION_COOKIE=holo_session', @@ -436,17 +438,45 @@ export function renderScaffoldEnvFiles( const cacheLines = normalizeScaffoldOptionalPackages(options.optionalPackages).includes('cache') ? [...renderCacheEnvFiles('file').env] : [] - const env = [...baseLines, ...driverLines, ...storageLines, ...authLines, ...cacheLines, ''].join('\n') + const mailLines = renderMailEnvFiles().env + const env = [...baseLines, ...driverLines, ...storageLines, ...authLines, ...cacheLines, ...mailLines, ''].join('\n') const example = [ '# Copy this file to .env and fill in your local values.', '# Supported layered env files: .env.local, .env.development, .env.production, .env.prod, .env.test', - ...[...baseLines, ...driverLines, ...storageLines, ...authLines, ...cacheLines].map(line => `${line.split('=')[0]}=`), + ...[...baseLines, ...driverLines, ...storageLines, ...authLines, ...cacheLines, ...renderMailEnvFiles().example].map(line => `${line.split('=')[0]}=`), '', ].join('\n') return { env, example } } +export function renderMailEnvFiles(): { env: readonly string[], example: readonly string[] } { + return { + env: [ + 'MAIL_MAILER=preview', + 'MAIL_FROM_ADDRESS=hello@app.test', + 'MAIL_FROM_NAME=Holo App', + 'MAIL_LOG_BODIES=false', + 'MAIL_HOST=127.0.0.1', + 'MAIL_PORT=1025', + 'MAIL_SECURE=false', + 'MAIL_USERNAME=', + 'MAIL_PASSWORD=', + ], + example: [ + 'MAIL_MAILER=', + 'MAIL_FROM_ADDRESS=', + 'MAIL_FROM_NAME=', + 'MAIL_LOG_BODIES=', + 'MAIL_HOST=', + 'MAIL_PORT=', + 'MAIL_SECURE=', + 'MAIL_USERNAME=', + 'MAIL_PASSWORD=', + ], + } +} + function renderRedisConnectionEnvFiles(): { env: readonly string[], example: readonly string[] } { return { env: [ diff --git a/packages/cli/src/project/shared.ts b/packages/cli/src/project/shared.ts index 494dba5..ccb90ac 100644 --- a/packages/cli/src/project/shared.ts +++ b/packages/cli/src/project/shared.ts @@ -254,6 +254,8 @@ export type MailInstallResult = { readonly updatedPackageJson: boolean readonly createdMailConfig: boolean readonly createdMailDirectory: boolean + readonly updatedEnv: boolean + readonly updatedEnvExample: boolean } export type SecurityInstallResult = { diff --git a/packages/cli/tests/cli.test.ts b/packages/cli/tests/cli.test.ts index 7078cf6..cdec630 100644 --- a/packages/cli/tests/cli.test.ts +++ b/packages/cli/tests/cli.test.ts @@ -1158,6 +1158,24 @@ export default { storageDefaultDisk: 'local', optionalPackages: ['auth'], }).env).toContain('SESSION_CONNECTION=main') + expect(projectInternals.renderScaffoldEnvFiles({ + projectName: 'Mail App', + databaseDriver: 'sqlite', + storageDefaultDisk: 'local', + }).env).toContain('MAIL_MAILER=preview') + expect(projectInternals.renderScaffoldEnvFiles({ + projectName: 'Mail App', + databaseDriver: 'sqlite', + storageDefaultDisk: 'local', + }).env).toContain('MAIL_USERNAME=') + expect(projectInternals.renderScaffoldEnvFiles({ + projectName: 'Mail App', + databaseDriver: 'sqlite', + storageDefaultDisk: 'local', + }).example).toContain('MAIL_FROM_ADDRESS=') + expect(projectInternals.renderMailConfig()).toContain('logBodies: env(\'MAIL_LOG_BODIES\', false)') + expect(projectInternals.renderMailConfig()).toContain('user: env(\'MAIL_USERNAME\') || undefined') + expect(projectInternals.renderMailConfig()).toContain('password: env(\'MAIL_PASSWORD\') || undefined') expect(projectInternals.renderScaffoldPackageJson({ projectName: 'nuxt-broadcast-app', framework: 'nuxt', @@ -2374,11 +2392,15 @@ export default defineAppConfig({ updatedPackageJson: true, createdMailConfig: true, createdMailDirectory: true, + updatedEnv: true, + updatedEnvExample: true, }) await expect(projectInternals.installMailIntoProject(projectRoot)).resolves.toMatchObject({ updatedPackageJson: false, createdMailConfig: false, createdMailDirectory: false, + updatedEnv: false, + updatedEnvExample: false, }) await expect(projectInternals.installSecurityIntoProject(projectRoot)).resolves.toMatchObject({ updatedPackageJson: true, @@ -7849,6 +7871,8 @@ export default defineEvent({ name: 'audit.activity' }) updatedPackageJson: true, createdMailConfig: true, createdMailDirectory: true, + updatedEnv: true, + updatedEnvExample: true, })) const installSecurityIntoProject = vi.fn(async () => ({ updatedPackageJson: true, @@ -8400,6 +8424,8 @@ export default defineEvent({ name: 'audit.activity' }) updatedPackageJson: false, createdMailConfig: false, createdMailDirectory: false, + updatedEnv: false, + updatedEnvExample: false, })) vi.resetModules() diff --git a/packages/cli/tests/mail-scaffold.test.ts b/packages/cli/tests/mail-scaffold.test.ts new file mode 100644 index 0000000..fd2a658 --- /dev/null +++ b/packages/cli/tests/mail-scaffold.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest' +import { renderMailConfig, renderMailEnvFiles, renderScaffoldEnvFiles } from '../src/project/scaffold' + +describe('@holo-js/cli mail scaffold', () => { + it('includes mail env defaults in scaffolded env files', () => { + const rendered = renderScaffoldEnvFiles({ + projectName: 'Mail App', + databaseDriver: 'sqlite', + storageDefaultDisk: 'local', + }) + + expect(rendered.env).toContain('MAIL_MAILER=preview') + expect(rendered.env).toContain('MAIL_FROM_ADDRESS=hello@app.test') + expect(rendered.env).toContain('MAIL_LOG_BODIES=false') + expect(rendered.env).toContain('MAIL_HOST=127.0.0.1') + expect(rendered.env).toContain('MAIL_USERNAME=') + expect(rendered.example).toContain('MAIL_MAILER=') + expect(rendered.example).toContain('MAIL_FROM_ADDRESS=') + expect(rendered.example).toContain('MAIL_PASSWORD=') + }) + + it('exposes a complete mail env block and matching config references', () => { + expect(renderMailEnvFiles().env).toEqual([ + 'MAIL_MAILER=preview', + 'MAIL_FROM_ADDRESS=hello@app.test', + 'MAIL_FROM_NAME=Holo App', + 'MAIL_LOG_BODIES=false', + 'MAIL_HOST=127.0.0.1', + 'MAIL_PORT=1025', + 'MAIL_SECURE=false', + 'MAIL_USERNAME=', + 'MAIL_PASSWORD=', + ]) + + const config = renderMailConfig() + expect(config).toContain('logBodies: env(\'MAIL_LOG_BODIES\', false)') + expect(config).toContain('host: env(\'MAIL_HOST\', \'127.0.0.1\')') + expect(config).toContain('user: env(\'MAIL_USERNAME\') || undefined') + expect(config).toContain('password: env(\'MAIL_PASSWORD\') || undefined') + }) +}) diff --git a/packages/config/src/defaults.ts b/packages/config/src/defaults.ts index 6ca2e61..cfa0f62 100644 --- a/packages/config/src/defaults.ts +++ b/packages/config/src/defaults.ts @@ -378,6 +378,8 @@ export const DEFAULT_AUTH_PASSWORD_BROKER = 'users' export const DEFAULT_AUTH_PASSWORD_RESET_TABLE = 'password_reset_tokens' export const DEFAULT_AUTH_PASSWORD_EXPIRE = 60 export const DEFAULT_AUTH_PASSWORD_THROTTLE = 60 +export const DEFAULT_AUTH_PASSWORD_RESET_ROUTE = '/reset-password' +export const DEFAULT_AUTH_EMAIL_VERIFICATION_ROUTE = '/verify-email' export const DEFAULT_WORKOS_SESSION_COOKIE = 'wos-session' export const DEFAULT_CLERK_SESSION_COOKIE = '__session' @@ -407,10 +409,12 @@ export const holoAuthDefaults: Readonly = Object.freez table: DEFAULT_AUTH_PASSWORD_RESET_TABLE, expire: DEFAULT_AUTH_PASSWORD_EXPIRE, throttle: DEFAULT_AUTH_PASSWORD_THROTTLE, + route: DEFAULT_AUTH_PASSWORD_RESET_ROUTE, }), }), emailVerification: Object.freeze({ required: false, + route: DEFAULT_AUTH_EMAIL_VERIFICATION_ROUTE, }), personalAccessTokens: Object.freeze({ defaultAbilities: Object.freeze([]), @@ -1406,6 +1410,7 @@ function normalizePasswordBroker( throttle: parseInteger(config.throttle, DEFAULT_AUTH_PASSWORD_THROTTLE, `auth password broker "${name}" throttle`, { minimum: 0, }), + route: config.route?.trim() || DEFAULT_AUTH_PASSWORD_RESET_ROUTE, }) /* v8 ignore stop */ } @@ -1582,6 +1587,9 @@ export function normalizeAuthConfig( required: typeof config.emailVerification === 'boolean' ? config.emailVerification : config.emailVerification?.required ?? false, + route: typeof config.emailVerification === 'boolean' + ? DEFAULT_AUTH_EMAIL_VERIFICATION_ROUTE + : config.emailVerification?.route?.trim() || DEFAULT_AUTH_EMAIL_VERIFICATION_ROUTE, }), personalAccessTokens: Object.freeze({ defaultAbilities: Object.freeze([...(config.personalAccessTokens?.defaultAbilities ?? [])]), diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index b9d05a3..431a034 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -594,10 +594,12 @@ export interface AuthPasswordBrokerConfig { readonly table?: string readonly expire?: number | string readonly throttle?: number | string + readonly route?: string } export interface AuthEmailVerificationConfig { readonly required?: boolean + readonly route?: string } export interface AuthPersonalAccessTokenConfig { @@ -671,6 +673,7 @@ export interface NormalizedAuthPasswordBrokerConfig { readonly table: string readonly expire: number readonly throttle: number + readonly route: string } export interface NormalizedAuthSocialProviderConfig { @@ -719,6 +722,7 @@ export interface NormalizedHoloAuthConfig { readonly passwords: Readonly> readonly emailVerification: { readonly required: boolean + readonly route: string } readonly personalAccessTokens: { readonly defaultAbilities: readonly string[] diff --git a/packages/core/src/adapter.ts b/packages/core/src/adapter.ts index 5855433..c495764 100644 --- a/packages/core/src/adapter.ts +++ b/packages/core/src/adapter.ts @@ -160,6 +160,7 @@ export function resolveHoloFrameworkOptions( preferCache: options.preferCache ?? processEnv.NODE_ENV === 'production', processEnv, renderView: options.renderView, + authRequest: options.authRequest, registerProjectQueueJobs: options.registerProjectQueueJobs, }, } @@ -237,6 +238,7 @@ async function initializeSingletonFrameworkProject< configureDB(currentRuntime.manager) await reconfigureOptionalHoloSubsystems(state.project.projectRoot, currentRuntime.loadedConfig, { renderView: resolved.runtime.renderView, + authRequest: resolved.runtime.authRequest, }) if (state.project.runtime !== currentRuntime) { @@ -348,6 +350,7 @@ export async function initializeHoloAdapterProject(project.projectRoot, options) await reconfigureOptionalHoloSubsystems(project.projectRoot, runtime.loadedConfig, { renderView: options.renderView, + authRequest: options.authRequest, }) return { diff --git a/packages/core/src/portable/holo.ts b/packages/core/src/portable/holo.ts index 6d0bcc5..5f24042 100644 --- a/packages/core/src/portable/holo.ts +++ b/packages/core/src/portable/holo.ts @@ -18,6 +18,7 @@ import { import { configureDB, DB, + Entity, resetDB, } from '@holo-js/db' import { importBundledRuntimeModule } from '../runtimeModule' @@ -42,6 +43,19 @@ interface CoreNotificationDatabaseRoute { readonly type: string } +type HoloAuthResult = { + readonly data: TData + readonly error: null +} | { + readonly data: null + readonly error: { + readonly code: string + readonly message: string + readonly status: number + readonly fields: Readonly>> + } +} + async function preloadGeneratedSchemaModule( projectRoot: string, registry: GeneratedProjectRegistry | undefined, @@ -121,13 +135,13 @@ export interface HoloAuthRuntimeBinding { login(credentials: Readonly> & { readonly password: string readonly remember?: boolean - }): Promise<{ + }): Promise + }>> loginUsing( user: unknown, options?: { @@ -188,7 +202,7 @@ export interface HoloAuthRuntimeBinding { readonly password: string readonly passwordConfirmation: string readonly remember?: boolean - }): Promise + }): Promise> logoutAll(guardName?: string): Promise> & { readonly password: string readonly remember?: boolean - }): Promise<{ + }): Promise + }>> loginUsing( user: unknown, options?: { @@ -281,16 +295,23 @@ export interface HoloAuthRuntimeBinding { } verification: { create(user: unknown, options?: { readonly guard?: string, readonly expiresAt?: Date }): Promise - consume(plainTextToken: string): Promise - } - passwords: { - request(email: string, options?: { readonly broker?: string, readonly expiresAt?: Date }): Promise - consume(input: { - readonly token: string - readonly password: string - readonly passwordConfirmation: string - }): Promise + resend(options?: { readonly guard?: string, readonly expiresAt?: Date, readonly email?: string }): Promise> + consume(plainTextToken: string): Promise> } + requestPasswordReset( + input: { + readonly email: string + }, + options?: { + readonly broker?: string + readonly expiresAt?: Date + }, + ): Promise> + resetPassword(input: { + readonly token: string + readonly password: string + readonly passwordConfirmation: string + }): Promise> } export interface HoloQueueRuntimeBinding { @@ -669,6 +690,8 @@ type AuthModule = { setSessionId(guardName: string, sessionId?: string): void getCachedUser(guardName: string): unknown setCachedUser(guardName: string, user: unknown): void + getRequestCookie?(name: string): string | undefined | Promise + getRequestHeader?(name: string): string | undefined | Promise getAccessToken?(guardName: string): string | undefined setAccessToken?(guardName: string, token?: string): void getRememberToken?(guardName: string): string | undefined @@ -681,6 +704,8 @@ type AuthModule = { setSessionId(guardName: string, sessionId?: string): void getCachedUser(guardName: string): unknown setCachedUser(guardName: string, user: unknown): void + getRequestCookie?(name: string): string | undefined | Promise + getRequestHeader?(name: string): string | undefined | Promise getAccessToken?(guardName: string): string | undefined setAccessToken?(guardName: string, token?: string): void getRememberToken?(guardName: string): string | undefined @@ -797,6 +822,10 @@ export interface CreateHoloOptions { readonly processEnv?: NodeJS.ProcessEnv readonly registerProjectQueueJobs?: boolean readonly renderView?: HoloServerViewRenderer + readonly authRequest?: { + readonly getCookie?: (name: string) => string | undefined | Promise + readonly getHeader?: (name: string) => string | undefined | Promise + } } export interface HoloRuntime { @@ -1308,24 +1337,50 @@ function bindAuthRuntimeToContext( activate() return runtime.verification.create(user, options) }, - consume(plainTextToken: Parameters[0]) { + resend(options?: Parameters[0]) { activate() - return runtime.verification.consume(plainTextToken) + return runtime.verification.resend(options) }, - }), - passwords: Object.freeze({ - request( - email: Parameters[0], - options?: Parameters[1], - ) { - activate() - return runtime.passwords.request(email, options) - }, - consume(input: Parameters[0]) { + consume(plainTextToken: Parameters[0]) { activate() - return runtime.passwords.consume(input) + return runtime.verification.consume(plainTextToken) }, }), + requestPasswordReset( + input: Parameters[0], + options?: Parameters[1], + ) { + activate() + return runtime.requestPasswordReset(input, options) + }, + resetPassword(input: Parameters[0]) { + activate() + return runtime.resetPassword(input) + }, + }) +} + +function attachAuthRequestAccessors( + context: TContext, + accessors: NonNullable, +): TContext & { + getRequestCookie?(name: string): string | undefined | Promise + getRequestHeader?(name: string): string | undefined | Promise +} { + return Object.freeze({ + ...context, + getRequestCookie: accessors.getCookie, + getRequestHeader: accessors.getHeader, }) } @@ -1918,8 +1973,19 @@ function createCoreNotificationStore( return Object.freeze(store) } +const authEmailDateFormatter = new Intl.DateTimeFormat('en-US', { + dateStyle: 'long', + timeStyle: 'short', + timeZone: 'UTC', +}) + +function formatAuthEmailExpiration(expiresAt: Date): string { + return `${authEmailDateFormatter.format(expiresAt)} UTC` +} + function createAuthNotificationsDeliveryHook( notificationsModule: NotificationsModule, + appUrl: string, ): { sendEmailVerification(input: { readonly provider: string @@ -1930,8 +1996,10 @@ function createAuthNotificationsDeliveryHook( readonly plainTextToken: string readonly expiresAt: Date } + readonly route: string }): Promise sendPasswordReset(input: { + readonly broker: string readonly provider: string readonly email: string readonly token: { @@ -1939,6 +2007,7 @@ function createAuthNotificationsDeliveryHook( readonly plainTextToken: string readonly expiresAt: Date } + readonly route: string }): Promise } { return Object.freeze({ @@ -1946,6 +2015,14 @@ function createAuthNotificationsDeliveryHook( const recipientName = typeof (input.user as { name?: unknown })?.name === 'string' ? (input.user as { name?: string }).name?.trim() : undefined + const lines = [ + 'Confirm your account to finish signing in.', + `This verification link expires at ${formatAuthEmailExpiration(input.token.expiresAt)}.`, + ] as const + const action = { + label: 'Verify email address', + url: createAuthActionUrl(appUrl, input.route, input.token.plainTextToken), + } as const const notification = notificationsModule.defineNotification({ type: 'auth.email-verification', via() { @@ -1956,12 +2033,14 @@ function createAuthNotificationsDeliveryHook( return { subject: 'Verify your email address', ...(recipientName ? { greeting: `Hello ${recipientName},` } : {}), - lines: [ - 'Use this token to verify your email address:', - input.token.plainTextToken, - `Provider: ${input.provider}`, - `Expires at: ${input.token.expiresAt.toISOString()}`, - ], + lines, + action, + html: createAuthEmailHtml({ + subject: 'Verify your email address', + ...(recipientName ? { greeting: `Hello ${recipientName},` } : {}), + lines, + action, + }), metadata: { provider: input.provider, tokenId: input.token.id, @@ -1982,6 +2061,14 @@ function createAuthNotificationsDeliveryHook( .notify(notification) }, async sendPasswordReset(input): Promise { + const lines = [ + 'Click the link below to choose a new password.', + `This reset link expires at ${formatAuthEmailExpiration(input.token.expiresAt)}.`, + ] as const + const action = { + label: 'Reset password', + url: createAuthActionUrl(appUrl, input.route, input.token.plainTextToken), + } as const const notification = notificationsModule.defineNotification({ type: 'auth.password-reset', via() { @@ -1991,12 +2078,13 @@ function createAuthNotificationsDeliveryHook( email() { return { subject: 'Reset your password', - lines: [ - 'Use this token to reset your password:', - input.token.plainTextToken, - `Provider: ${input.provider}`, - `Expires at: ${input.token.expiresAt.toISOString()}`, - ], + lines, + action, + html: createAuthEmailHtml({ + subject: 'Reset your password', + lines, + action, + }), metadata: { provider: input.provider, tokenId: input.token.id, @@ -2217,6 +2305,73 @@ function createNotificationMailText(message: { return parts.length > 0 ? parts.join('\n\n') : undefined } +function joinAppUrl(baseUrl: string, path: string): string { + const normalizedBaseUrl = baseUrl.endsWith('/') + ? baseUrl.slice(0, -1) + : baseUrl + const normalizedPath = path.startsWith('/') + ? path + : `/${path}` + + return `${normalizedBaseUrl}${normalizedPath}` +} + +function createAuthActionUrl( + appUrl: string, + path: string, + token: string, +): string { + const url = new URL(joinAppUrl(appUrl, path)) + url.searchParams.set('token', token) + return url.toString() +} + +function escapeAuthEmailHtml(value: string): string { + return value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", ''') +} + +function createAuthEmailHtml(message: { + readonly subject: string + readonly greeting?: string + readonly lines: readonly string[] + readonly action: { + readonly label: string + readonly url: string + } +}): string { + const sections = [ + typeof message.greeting === 'string' + ? `

${escapeAuthEmailHtml(message.greeting)}

` + : '', + ...message.lines.map(line => `

${escapeAuthEmailHtml(line)}

`), + `

` + + `` + + `${escapeAuthEmailHtml(message.action.label)}` + + `

`, + `

` + + `If the button does not work, open this link: ` + + `${escapeAuthEmailHtml(message.action.url)}` + + `

`, + ].join('') + + return [ + '', + '', + `${escapeAuthEmailHtml(message.subject)}`, + '', + '
', + `

${escapeAuthEmailHtml(message.subject)}

`, + sections, + '
', + ].join('') +} + function createCoreNotificationMailSender( mailModule: MailModule, ): { @@ -2261,6 +2416,7 @@ function createCoreNotificationMailSender( function createAuthMailDeliveryHook( mailModule: MailModule, + appUrl: string, ): { sendEmailVerification(input: { readonly provider: string @@ -2271,8 +2427,10 @@ function createAuthMailDeliveryHook( readonly plainTextToken: string readonly expiresAt: Date } + readonly route: string }): Promise sendPasswordReset(input: { + readonly broker: string readonly provider: string readonly email: string readonly token: { @@ -2280,6 +2438,7 @@ function createAuthMailDeliveryHook( readonly plainTextToken: string readonly expiresAt: Date } + readonly route: string }): Promise } { return Object.freeze({ @@ -2287,6 +2446,14 @@ function createAuthMailDeliveryHook( const recipientName = typeof (input.user as { name?: unknown })?.name === 'string' ? (input.user as { name?: string }).name?.trim() : undefined + const lines = [ + 'Confirm your account to finish signing in.', + `This verification link expires at ${formatAuthEmailExpiration(input.token.expiresAt)}.`, + ] as const + const action = { + label: 'Verify email address', + url: createAuthActionUrl(appUrl, input.route, input.token.plainTextToken), + } as const await mailModule.sendMail({ to: { @@ -2294,13 +2461,17 @@ function createAuthMailDeliveryHook( ...(recipientName ? { name: recipientName } : {}), }, subject: 'Verify your email address', - text: [ - recipientName ? `Hello ${recipientName},` : undefined, - 'Use this token to verify your email address:', - input.token.plainTextToken, - `Provider: ${input.provider}`, - `Expires at: ${input.token.expiresAt.toISOString()}`, - ].filter((value): value is string => typeof value === 'string').join('\n\n'), + html: createAuthEmailHtml({ + subject: 'Verify your email address', + ...(recipientName ? { greeting: `Hello ${recipientName},` } : {}), + lines, + action, + }), + text: createNotificationMailText({ + ...(recipientName ? { greeting: `Hello ${recipientName},` } : {}), + lines, + action, + }), metadata: { provider: input.provider, tokenId: input.token.id, @@ -2308,15 +2479,27 @@ function createAuthMailDeliveryHook( }) }, async sendPasswordReset(input): Promise { + const lines = [ + 'Click the link below to choose a new password.', + `This reset link expires at ${formatAuthEmailExpiration(input.token.expiresAt)}.`, + ] as const + const action = { + label: 'Reset password', + url: createAuthActionUrl(appUrl, input.route, input.token.plainTextToken), + } as const + await mailModule.sendMail({ to: input.email, subject: 'Reset your password', - text: [ - 'Use this token to reset your password:', - input.token.plainTextToken, - `Provider: ${input.provider}`, - `Expires at: ${input.token.expiresAt.toISOString()}`, - ].join('\n\n'), + html: createAuthEmailHtml({ + subject: 'Reset your password', + lines, + action, + }), + text: createNotificationMailText({ + lines, + action, + }), metadata: { provider: input.provider, tokenId: input.token.id, @@ -3026,6 +3209,14 @@ async function createCoreAuthProviders( first(): Promise } + type AuthModelEntity = { + forceFill?(values: Record): unknown + } + + type AuthModelRepository = { + saveEntity?(entity: unknown, internalColumns?: ReadonlySet): Promise + } + const resolvedModule = await resolveAuthProviderRuntime(projectRoot, loadedConfig, providerConfig.model) as { default?: unknown holoModelPendingSchema?: boolean @@ -3047,6 +3238,7 @@ async function createCoreAuthProviders( query?(): AuthModelQuery find(value: unknown): Promise where(column: string, value: unknown): AuthModelQuery + getRepository?(): AuthModelRepository create(values: Record): Promise update(id: unknown, values: Record): Promise } @@ -3131,7 +3323,9 @@ async function createCoreAuthProviders( } const prepareAuthCreateInput = async (input: Readonly>): Promise> => { - const sanitizedInput = sanitizeAuthWriteInput(input) + const sanitizedInput = sanitizeAuthWriteInput(input, { + enforceFillable: false, + }) if (typeof resolvedModule.prepareAuthCreateInput !== 'function') { return sanitizedInput } @@ -3145,7 +3339,9 @@ async function createCoreAuthProviders( user: unknown, input: Readonly>, ): Promise> => { - const sanitizedInput = sanitizeAuthWriteInput(input) + const sanitizedInput = sanitizeAuthWriteInput(input, { + enforceFillable: false, + }) if (typeof resolvedModule.prepareAuthUpdateInput !== 'function') { return sanitizedInput } @@ -3155,6 +3351,25 @@ async function createCoreAuthProviders( }) } + const saveAuthEntity = async (entity: unknown, values: Record) => { + const repository = typeof model.getRepository === 'function' + ? model.getRepository() + : null + + if ( + repository + && typeof repository.saveEntity === 'function' + && entity + && typeof entity === 'object' + && typeof (entity as AuthModelEntity).forceFill === 'function' + ) { + ;(entity as AuthModelEntity).forceFill!(values) + return repository.saveEntity(entity, new Set(Object.keys(values))) + } + + return null + } + const adapter = { async findById(id: string | number) { const resolved = await model.find(id) @@ -3187,14 +3402,27 @@ async function createCoreAuthProviders( return resolved ? markProviderUser(resolved, providerName) : null }, async create(input: Readonly>) { - return markProviderUser(await model.create(await prepareAuthCreateInput(input)), providerName) + const values = await prepareAuthCreateInput(input) + const repository = typeof model.getRepository === 'function' + ? model.getRepository() + : null + const entity = repository && typeof repository.saveEntity === 'function' + ? new Entity(repository as never, values as never, false) + : null + const persisted = entity ? await saveAuthEntity(entity, values) : null + + return markProviderUser(persisted ?? await model.create(values), providerName) }, /* v8 ignore start -- adapter shape mirrors the auth package contract; core tests cover the wired runtime behavior */ async update(user: unknown, input: Readonly>) { - return markProviderUser( - await model.update(getEntityAttributes(user).id, await prepareAuthUpdateInput(user, input)), - providerName, - ) + const id = getEntityAttributes(user).id + const values = await prepareAuthUpdateInput(user, input) + const existing = typeof model.find === 'function' + ? await model.find(id) + : null + const persisted = existing ? await saveAuthEntity(existing, values) : null + + return markProviderUser(persisted ?? await model.update(id, values), providerName) }, matchesUser(user: unknown) { if (typeof model === 'function' && user instanceof model) { @@ -3565,6 +3793,7 @@ export async function reconfigureOptionalHoloSubsystems, options: { readonly renderView?: HoloServerViewRenderer + readonly authRequest?: CreateHoloOptions['authRequest'] } = {}, ): Promise<{ readonly queueModule?: QueueModule @@ -3854,7 +4083,10 @@ export async function reconfigureOptionalHoloSubsystems( const optionalSubsystems = await reconfigureOptionalHoloSubsystems(projectRoot, loadedConfig, { renderView: options.renderView, + authRequest: options.authRequest, }) activeQueueModule = optionalSubsystems.queueModule activeSessionRuntime = optionalSubsystems.session diff --git a/packages/core/tests/adapter.test.ts b/packages/core/tests/adapter.test.ts index 2e9ae9b..8a6e86f 100644 --- a/packages/core/tests/adapter.test.ts +++ b/packages/core/tests/adapter.test.ts @@ -725,7 +725,10 @@ export default { password: 'secret', passwordConfirmation: 'secret', })).resolves.toMatchObject({ - role: 'first', + data: { + role: 'first', + }, + error: null, }) await new Promise(resolvePromise => setTimeout(resolvePromise, 25)) @@ -771,7 +774,10 @@ export default { password: 'secret', passwordConfirmation: 'secret', })).resolves.toMatchObject({ - role: 'second', + data: { + role: 'second', + }, + error: null, }) }) diff --git a/packages/core/tests/auth-runtime.test.ts b/packages/core/tests/auth-runtime.test.ts index 28b146d..0d811f3 100644 --- a/packages/core/tests/auth-runtime.test.ts +++ b/packages/core/tests/auth-runtime.test.ts @@ -20,6 +20,26 @@ type SessionRecordLike = { readonly rememberTokenHash?: string } +function unwrapAuthResult(result: { + readonly data: TData + readonly error: null +} | { + readonly data: null + readonly error: { + readonly code: string + } +} | null | undefined): TData { + if (!result) { + throw new Error('Expected auth result but received nothing.') + } + + if (result.error) { + throw new Error(`Expected auth success but received ${result.error.code}.`) + } + + return result.data +} + async function createProject(options: { session?: 'file' | 'database' | false auth?: boolean @@ -318,23 +338,27 @@ export default { table.index(['email']) }) - const registered = await runtime.auth?.register({ + const registered = unwrapAuthResult(await runtime.auth?.register({ name: 'Ava', email: 'ava@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) const verificationToken = await runtime.auth?.verification.create(registered!) as VerificationTokenLike | undefined - await runtime.auth?.passwords.request('ava@example.com') + await runtime.auth?.requestPasswordReset({ email: 'ava@example.com' }) expect(mailer.send).toHaveBeenCalledTimes(2) expect(mailer.send).toHaveBeenNthCalledWith(1, expect.objectContaining({ subject: 'Verify your email address', lines: expect.arrayContaining([ - 'Use this token to verify your email address:', - verificationToken?.plainTextToken, + 'Confirm your account to finish signing in.', ]), + action: { + label: 'Verify email address', + url: `http://localhost:3000/verify-email?token=${encodeURIComponent(verificationToken?.plainTextToken ?? '')}`, + }, + html: expect.stringContaining(`/verify-email?token=${encodeURIComponent(verificationToken?.plainTextToken ?? '')}`), metadata: { provider: 'users', tokenId: verificationToken?.id, @@ -349,8 +373,12 @@ export default { expect(mailer.send).toHaveBeenNthCalledWith(2, expect.objectContaining({ subject: 'Reset your password', lines: expect.arrayContaining([ - 'Use this token to reset your password:', + 'Click the link below to choose a new password.', ]), + action: expect.objectContaining({ + label: 'Reset password', + url: expect.stringContaining('/reset-password?token='), + }), metadata: expect.objectContaining({ provider: 'users', }), @@ -442,23 +470,26 @@ export default { table.index(['email']) }) - const registered = await runtime.auth?.register({ + const registered = unwrapAuthResult(await runtime.auth?.register({ name: 'Ava', email: 'ava@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) const verificationToken = await runtime.auth?.verification.create(registered!) as VerificationTokenLike | undefined - await runtime.auth?.passwords.request('ava@example.com') + await runtime.auth?.requestPasswordReset({ email: 'ava@example.com' }) expect(mailer.send).toHaveBeenCalledTimes(2) expect(mailer.send).toHaveBeenNthCalledWith(1, expect.objectContaining({ subject: 'Verify your email address', lines: expect.arrayContaining([ - 'Use this token to verify your email address:', - verificationToken?.plainTextToken, + 'Confirm your account to finish signing in.', ]), + action: { + label: 'Verify email address', + url: `http://localhost:3000/verify-email?token=${encodeURIComponent(verificationToken?.plainTextToken ?? '')}`, + }, }), expect.objectContaining({ channel: 'email', route: { @@ -469,8 +500,12 @@ export default { expect(mailer.send).toHaveBeenNthCalledWith(2, expect.objectContaining({ subject: 'Reset your password', lines: expect.arrayContaining([ - 'Use this token to reset your password:', + 'Click the link below to choose a new password.', ]), + action: expect.objectContaining({ + label: 'Reset password', + url: expect.stringContaining('/reset-password?token='), + }), }), expect.objectContaining({ channel: 'email', route: 'ava@example.com', @@ -519,12 +554,12 @@ export default { table.index(['email']) }) - const registered = await runtime.auth?.register({ + const registered = unwrapAuthResult(await runtime.auth?.register({ name: 'Ava', email: 'ava@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) await expect(runtime.auth?.verification.create(registered!)).resolves.toMatchObject({ email: 'ava@example.com', @@ -567,12 +602,12 @@ export default { table.index(['email']) }) - const registered = await runtime.auth?.register({ + const registered = unwrapAuthResult(await runtime.auth?.register({ name: 'Ava', email: 'ava@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) await expect(runtime.auth?.verification.create(registered!)).resolves.toMatchObject({ email: 'ava@example.com', @@ -659,15 +694,15 @@ export default { table.index(['email']) }) - const registered = await runtime.auth?.register({ + const registered = unwrapAuthResult(await runtime.auth?.register({ name: 'Ava', email: 'ava@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) const verificationToken = await runtime.auth?.verification.create(registered!) as VerificationTokenLike | undefined - await runtime.auth?.passwords.request('ava@example.com') + await runtime.auth?.requestPasswordReset({ email: 'ava@example.com' }) expect(listFakeSentMails()).toHaveLength(2) expect(listFakeSentMails()[0]!.mail).toMatchObject({ @@ -678,7 +713,8 @@ export default { name: 'Ava', }, ], - text: expect.stringContaining(verificationToken?.plainTextToken ?? ''), + text: expect.stringContaining(`Verify email address: http://localhost:3000/verify-email?token=${encodeURIComponent(verificationToken?.plainTextToken ?? '')}`), + html: expect.stringContaining(`/verify-email?token=${encodeURIComponent(verificationToken?.plainTextToken ?? '')}`), metadata: expect.objectContaining({ provider: 'users', tokenId: verificationToken?.id, @@ -691,7 +727,8 @@ export default { email: 'ava@example.com', }, ], - text: expect.stringContaining('Use this token to reset your password:'), + text: expect.stringContaining('Reset password: http://localhost:3000/reset-password?token='), + html: expect.stringContaining('/reset-password?token='), metadata: expect.objectContaining({ provider: 'users', }), @@ -773,12 +810,12 @@ export default { await runtime.initialize() - const registered = await runtime.auth?.register({ + const registered = unwrapAuthResult(await runtime.auth?.register({ name: 'Ava', email: 'ava@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) expect(registered).not.toHaveProperty('password') await runtime.auth?.login({ @@ -2084,12 +2121,48 @@ export default { email_verified_at: {}, }, }, - fillable: ['email', 'name', 'password', 'email_verified_at'], + fillable: ['email', 'name', 'password'], guarded: ['id'], hasExplicitFillable: true, }, + getRepository() { + return { + async saveEntity(entity, internalColumns) { + if (!internalColumns.has('email_verified_at')) { + throw new Error('Expected auth updates to treat email_verified_at as an internal column.') + } + + const attributes = typeof entity?.toAttributes === 'function' + ? entity.toAttributes() + : entity + const id = typeof attributes.id === 'number' ? attributes.id : 1 + const record = { + id, + public_id: attributes.public_id, + team_id: attributes.team_id, + email: attributes.email, + name: attributes.name, + password: attributes.password, + email_verified_at: attributes.email_verified_at, + } + records.set(id, record) + return record + }, + } + }, async find(id) { - return records.get(Number(id)) ?? null + const existing = records.get(Number(id)) + if (!existing) { + return null + } + + return { + ...existing, + forceFill(values) { + Object.assign(this, values) + return this + }, + } }, where() { return { @@ -2102,9 +2175,8 @@ export default { records.set(1, { id: 1, ...values }) return { id: 1, ...values } }, - async update(id, values) { - records.set(Number(id), { id: Number(id), ...values }) - return { id: Number(id), ...values } + async update() { + throw new Error('model.update should not be used for auth writes when saveEntity is available') }, } `, 'utf8') @@ -2124,7 +2196,7 @@ export default { name: 'Hosted User', avatar: 'https://cdn.test/avatar.png', email_verified_at: new Date('2026-04-11T00:00:00.000Z'), - })).resolves.toEqual({ + })).resolves.toMatchObject({ id: 1, public_id: 'generated-public-id', team_id: 7, @@ -2137,8 +2209,11 @@ export default { name: 'Updated Hosted User', avatar: 'https://cdn.test/avatar-2.png', email_verified_at: new Date('2026-04-12T00:00:00.000Z'), - })).resolves.toEqual({ + })).resolves.toMatchObject({ id: 1, + public_id: 'generated-public-id', + team_id: 7, + email: 'hosted@example.com', name: 'Updated Hosted User', email_verified_at: new Date('2026-04-12T00:00:00.000Z'), }) @@ -2594,12 +2669,12 @@ export default { await expect(runtime.session?.read(String(createdSession?.id))).resolves.toBeNull() await expect(runtime.session?.read('missing-session')).resolves.toBeNull() - const registered = await runtime.auth?.register({ + const registered = unwrapAuthResult(await runtime.auth?.register({ name: 'Ava', email: 'ava@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) expect(registered).toMatchObject({ email: 'ava@example.com', }) @@ -2705,17 +2780,17 @@ export default { table.timestamp('updated_at') }) - const registered = await runtime.auth?.register({ + const registered = unwrapAuthResult(await runtime.auth?.register({ name: 'Ava', email: 'ava@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) expect(registered).toMatchObject({ email: 'ava@example.com', }) - await runtime.auth?.passwords.request('ava@example.com') + await runtime.auth?.requestPasswordReset({ email: 'ava@example.com' }) const rows = await DB.table('admin_password_reset_tokens').get>() expect(rows).toHaveLength(1) }) @@ -2794,18 +2869,18 @@ export default { table.timestamp('updated_at') }) - const registered = await runtime.auth?.register({ + const registered = unwrapAuthResult(await runtime.auth?.register({ name: 'Ava', email: 'ava@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) expect(registered).toMatchObject({ email: 'ava@example.com', }) - await runtime.auth?.passwords.request('ava@example.com') + await runtime.auth?.requestPasswordReset({ email: 'ava@example.com' }) expect(listFakeSentMails()).toHaveLength(1) const firstTokenRows = await DB.table('password_reset_tokens').get>() @@ -2818,7 +2893,7 @@ export default { created_at: '2000-01-01T00:00:00.000Z', }) - await runtime.auth?.passwords.request('ava@example.com') + await runtime.auth?.requestPasswordReset({ email: 'ava@example.com' }) expect(listFakeSentMails()).toHaveLength(1) const finalTokenRows = await DB.table('password_reset_tokens').get>() @@ -2912,12 +2987,12 @@ export default { table.timestamps() }) - const registered = await runtime.auth?.register({ + const registered = unwrapAuthResult(await runtime.auth?.register({ name: 'Ava', email: 'ava@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) expect(registered).toMatchObject({ id: 'user-1', }) @@ -3403,12 +3478,12 @@ export default { }) await runtime.initialize() - const registered = await runtime.auth?.register({ + const registered = unwrapAuthResult(await runtime.auth?.register({ name: 'Admin', email: 'admin@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) expect(registered).toMatchObject({ id: 1, email: 'admin@example.com', @@ -3463,12 +3538,12 @@ export default { preferCache: false, }) await noDefaultRuntime.initialize() - const plainRegistered = await noDefaultRuntime.auth?.register({ + const plainRegistered = unwrapAuthResult(await noDefaultRuntime.auth?.register({ name: 'Plain', email: 'plain@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) expect(plainRegistered).toMatchObject({ id: 1, email: 'plain@example.com', @@ -3561,12 +3636,12 @@ export default { await runtime.initialize() - const registered = await runtime.auth?.register({ + const registered = unwrapAuthResult(await runtime.auth?.register({ name: 'Fallback User', email: 'fallback@example.com', password: 'supersecret', passwordConfirmation: 'supersecret', - }) + })) expect(registered).toMatchObject({ email: 'fallback@example.com', diff --git a/packages/core/tests/runtime.test.ts b/packages/core/tests/runtime.test.ts index 275f0a5..8eed6fd 100644 --- a/packages/core/tests/runtime.test.ts +++ b/packages/core/tests/runtime.test.ts @@ -485,7 +485,7 @@ describe('@holo-js/core portable runtime', () => { refreshUser: vi.fn(async () => ({ id: 2 })), id: vi.fn(async () => 3), currentAccessToken: vi.fn(async () => ({ id: 'token' })), - login: vi.fn(async () => ({ guard: 'web', user: { id: 1 }, sessionId: 'session', cookies: [] })), + login: vi.fn(async () => ({ data: { guard: 'web', user: { id: 1 }, sessionId: 'session', cookies: [] }, error: null })), loginUsing: vi.fn(async () => ({ guard: 'web', user: { id: 1 }, sessionId: 'session', cookies: [] })), loginUsingId: vi.fn(async () => ({ guard: 'web', user: { id: 1 }, sessionId: 'session', cookies: [] })), impersonate: vi.fn(async () => ({ guard: 'web', user: { id: 2 }, sessionId: 'session', cookies: [] })), @@ -503,7 +503,7 @@ describe('@holo-js/core portable runtime', () => { hashPassword: vi.fn(async () => 'digest'), verifyPassword: vi.fn(async () => true), needsPasswordRehash: vi.fn(async () => false), - login: vi.fn(async () => ({ guard: 'web', user: { id: 1 }, sessionId: 'session', cookies: [] })), + login: vi.fn(async () => ({ data: { guard: 'web', user: { id: 1 }, sessionId: 'session', cookies: [] }, error: null })), loginUsing: vi.fn(async () => ({ guard: 'web', user: { id: 1 }, sessionId: 'session', cookies: [] })), loginUsingId: vi.fn(async () => ({ guard: 'web', user: { id: 1 }, sessionId: 'session', cookies: [] })), impersonate: vi.fn(async () => ({ guard: 'web', user: { id: 2 }, sessionId: 'session', cookies: [] })), @@ -511,7 +511,7 @@ describe('@holo-js/core portable runtime', () => { impersonation: vi.fn(async () => ({ active: true })), stopImpersonating: vi.fn(async () => ({ id: 1 })), logout: vi.fn(async () => ({ guard: 'web', cookies: [] })), - register: vi.fn(async () => ({ id: 4 })), + register: vi.fn(async () => ({ data: { id: 4 }, error: null })), logoutAll: vi.fn(async () => [{ guard: 'web', cookies: [] }]), guard: vi.fn(() => guard), tokens: { @@ -524,12 +524,11 @@ describe('@holo-js/core portable runtime', () => { }, verification: { create: vi.fn(async () => ({ id: 'verify' })), - consume: vi.fn(async () => ({ id: 1 })), - }, - passwords: { - request: vi.fn(async () => {}), - consume: vi.fn(async () => ({ id: 1 })), + resend: vi.fn(async () => ({ data: { id: 'verify-resend' }, error: null })), + consume: vi.fn(async () => ({ data: { id: 1 }, error: null })), }, + requestPasswordReset: vi.fn(async () => ({ data: undefined, error: null })), + resetPassword: vi.fn(async () => ({ data: { id: 1 }, error: null })), } const bound = holoRuntimeInternals.bindAuthRuntimeToContext(runtime, { activate }) @@ -548,20 +547,26 @@ describe('@holo-js/core portable runtime', () => { await expect(bound.user()).resolves.toEqual({ id: 1 }) await expect(bound.refreshUser()).resolves.toEqual({ id: 2 }) await expect(bound.currentAccessToken()).resolves.toEqual({ id: 'token' }) - await expect(bound.login({ email: 'ava@example.com', password: 'secret' })).resolves.toMatchObject({ guard: 'web' }) + await expect(bound.login({ email: 'ava@example.com', password: 'secret' })).resolves.toEqual({ + data: { guard: 'web', user: { id: 1 }, sessionId: 'session', cookies: [] }, + error: null, + }) await expect(bound.logout()).resolves.toEqual({ guard: 'web', cookies: [] }) - await expect(bound.register({ email: 'ava@example.com', password: 'secret', passwordConfirmation: 'secret' })).resolves.toEqual({ id: 4 }) + await expect(bound.register({ email: 'ava@example.com', password: 'secret', passwordConfirmation: 'secret' })).resolves.toEqual({ + data: { id: 4 }, + error: null, + }) await expect(bound.logoutAll('web')).resolves.toEqual([{ guard: 'web', cookies: [] }]) await expect(bound.tokens.revoke()).resolves.toBeUndefined() await expect(bound.tokens.revokeAll({ id: 1 }, { guard: 'web' })).resolves.toBe(2) await expect(bound.tokens.authenticate('plain-text')).resolves.toEqual({ id: 1 }) await expect(bound.tokens.can('plain-text', 'orders.read')).resolves.toBe(true) - await expect(bound.verification.consume('verify-token')).resolves.toEqual({ id: 1 }) - await expect(bound.passwords.consume({ + await expect(bound.verification.consume('verify-token')).resolves.toEqual({ data: { id: 1 }, error: null }) + await expect(bound.resetPassword({ token: 'reset-token', password: 'secret', passwordConfirmation: 'secret', - })).resolves.toEqual({ id: 1 }) + })).resolves.toEqual({ data: { id: 1 }, error: null }) const boundGuard = bound.guard('admin') await expect(boundGuard.check()).resolves.toBe(true) @@ -569,7 +574,10 @@ describe('@holo-js/core portable runtime', () => { await expect(boundGuard.refreshUser()).resolves.toEqual({ id: 2 }) await expect(boundGuard.id()).resolves.toBe(3) await expect(boundGuard.currentAccessToken()).resolves.toEqual({ id: 'token' }) - await expect(boundGuard.login({ email: 'admin@example.com', password: 'secret' })).resolves.toMatchObject({ guard: 'web' }) + await expect(boundGuard.login({ email: 'admin@example.com', password: 'secret' })).resolves.toEqual({ + data: { guard: 'web', user: { id: 1 }, sessionId: 'session', cookies: [] }, + error: null, + }) await expect(boundGuard.loginUsing({ id: 1 }, { remember: true })).resolves.toMatchObject({ guard: 'web' }) await expect(boundGuard.loginUsingId(1, { remember: true })).resolves.toMatchObject({ guard: 'web' }) await expect(boundGuard.impersonate({ id: 2 }, { actorGuard: 'web' })).resolves.toMatchObject({ guard: 'web' }) @@ -582,7 +590,14 @@ describe('@holo-js/core portable runtime', () => { await expect(bound.tokens.create({ id: 1 }, { name: 'browser' })).resolves.toEqual({ id: 'created' }) await expect(bound.tokens.list({ id: 1 }, { guard: 'web' })).resolves.toEqual([{ id: 'listed' }]) await expect(bound.verification.create({ id: 1 }, { guard: 'web' })).resolves.toEqual({ id: 'verify' }) - await expect(bound.passwords.request('ava@example.com', { broker: 'users' })).resolves.toBeUndefined() + await expect(bound.verification.resend({ email: 'ava@example.com' })).resolves.toEqual({ + data: { id: 'verify-resend' }, + error: null, + }) + await expect(bound.requestPasswordReset({ email: 'ava@example.com' }, { broker: 'users' })).resolves.toEqual({ + data: undefined, + error: null, + }) }) it('does not require @holo-js/queue-db for the implicit default sync queue runtime', async () => { @@ -5554,7 +5569,7 @@ describe('@holo-js/core helper coverage', () => { async sendMail(message: unknown) { authMailSends.push(message) }, - } as never) + } as never, 'https://app.test') const verificationToken = { id: 'verify-token', plainTextToken: 'verify-plain', @@ -5566,14 +5581,17 @@ describe('@holo-js/core helper coverage', () => { user: { name: ' Ava ' }, email: 'ava@example.com', token: verificationToken, + route: '/verify-email', }) await authMailHook.sendEmailVerification({ provider: 'users', user: {}, email: 'no-name@example.com', token: verificationToken, + route: '/verify-email', }) await authMailHook.sendPasswordReset({ + broker: 'users', provider: 'users', email: 'reset@example.com', token: { @@ -5581,6 +5599,7 @@ describe('@holo-js/core helper coverage', () => { plainTextToken: 'reset-plain', expiresAt: new Date('2026-04-12T13:00:00.000Z'), }, + route: '/reset-password', }) expect(authMailSends[0]).toMatchObject({ @@ -5589,8 +5608,11 @@ describe('@holo-js/core helper coverage', () => { name: 'Ava', }, subject: 'Verify your email address', + html: expect.stringContaining('https://app.test/verify-email?token=verify-plain'), }) expect((authMailSends[0] as { text: string }).text).toContain('Hello Ava,') + expect((authMailSends[0] as { text: string }).text).toContain('This verification link expires at April 12, 2026 at 12:00 PM UTC.') + expect((authMailSends[0] as { text: string }).text).toContain('Verify email address: https://app.test/verify-email?token=verify-plain') expect(authMailSends[1]).toMatchObject({ to: { email: 'no-name@example.com', @@ -5601,7 +5623,9 @@ describe('@holo-js/core helper coverage', () => { expect(authMailSends[2]).toMatchObject({ to: 'reset@example.com', subject: 'Reset your password', + html: expect.stringContaining('https://app.test/reset-password?token=reset-plain'), }) + expect((authMailSends[2] as { text: string }).text).toContain('This reset link expires at April 12, 2026 at 1:00 PM UTC.') const notificationDeliveries: unknown[] = [] let emailVerificationRoute: unknown @@ -5632,21 +5656,24 @@ describe('@holo-js/core helper coverage', () => { }, } }, - } as never) + } as never, 'https://app.test') await authNotificationsHook.sendEmailVerification({ provider: 'users', user: { name: ' Ava ' }, email: 'ava@example.com', token: verificationToken, + route: '/verify-email', }) await authNotificationsHook.sendEmailVerification({ provider: 'users', user: {}, email: 'no-name@example.com', token: verificationToken, + route: '/verify-email', }) await authNotificationsHook.sendPasswordReset({ + broker: 'users', provider: 'users', email: 'reset@example.com', token: { @@ -5654,6 +5681,7 @@ describe('@holo-js/core helper coverage', () => { plainTextToken: 'reset-plain', expiresAt: new Date('2026-04-12T13:00:00.000Z'), }, + route: '/reset-password', }) expect(notificationDeliveries[0]).toMatchObject({ @@ -5663,8 +5691,15 @@ describe('@holo-js/core helper coverage', () => { }, message: { greeting: 'Hello Ava,', + action: { + label: 'Verify email address', + url: 'https://app.test/verify-email?token=verify-plain', + }, }, }) + expect((notificationDeliveries[0] as { message: { lines: readonly string[] } }).message.lines).toContain( + 'This verification link expires at April 12, 2026 at 12:00 PM UTC.', + ) expect(notificationDeliveries[1]).toMatchObject({ route: 'no-name@example.com', message: { @@ -5676,8 +5711,15 @@ describe('@holo-js/core helper coverage', () => { route: 'reset@example.com', message: { subject: 'Reset your password', + action: { + label: 'Reset password', + url: 'https://app.test/reset-password?token=reset-plain', + }, }, }) + expect((notificationDeliveries[2] as { message: { lines: readonly string[] } }).message.lines).toContain( + 'This reset link expires at April 12, 2026 at 1:00 PM UTC.', + ) }) it('boots mail with the shared render runtime when no explicit render option is passed', async () => { diff --git a/packages/forms/package.json b/packages/forms/package.json index 2c79f28..e94a878 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -10,6 +10,11 @@ "import": "./dist/index.mjs", "default": "./dist/index.mjs" }, + "./schema": { + "types": "./dist/schema.d.ts", + "import": "./dist/schema.mjs", + "default": "./dist/schema.mjs" + }, "./client": { "types": "./dist/client.d.ts", "import": "./dist/client.mjs", diff --git a/packages/forms/src/client-security.ts b/packages/forms/src/client-security.ts index 54e415d..bb01c19 100644 --- a/packages/forms/src/client-security.ts +++ b/packages/forms/src/client-security.ts @@ -1,5 +1,9 @@ -import { FormContractError } from './contracts' -import { formsSecurityInternals } from './security' +import { FormContractError } from './errors' +import { + createMissingSecurityPackageError, + isMissingOptionalPackageError, + parseCookieHeader, +} from './security-shared' type SecurityClientModule = { getSecurityClientConfig(): { @@ -41,8 +45,8 @@ export async function loadSecurityClientModule(): Promise .catch(async (error) => { securityClientModulePromise = undefined - if (formsSecurityInternals.isMissingOptionalPackageError(error)) { - throw formsSecurityInternals.createMissingSecurityPackageError() + if (isMissingOptionalPackageError(error)) { + throw createMissingSecurityPackageError() } throw error @@ -66,7 +70,7 @@ export async function getClientCsrfField(): Promise<{ readonly name: string, rea const security = await loadSecurityClientModule() const config = security.getSecurityClientConfig().csrf - const value = formsSecurityInternals.parseCookieHeader(runtime.document.cookie)[config.cookie] + const value = parseCookieHeader(runtime.document.cookie)[config.cookie] if (!value) { throw new FormContractError( diff --git a/packages/forms/src/client.ts b/packages/forms/src/client.ts index c0fd186..b9330fe 100644 --- a/packages/forms/src/client.ts +++ b/packages/forms/src/client.ts @@ -1,15 +1,25 @@ -import { - type FormFailurePayload, - type FormSchema, - type FormSubmissionResult, - type FormSuccessPayload, - type SerializedFormSubmission as SerializedSubmissionState, - type SerializedFormSubmission, - createFailedSubmission, - createSuccessfulSubmission, - validate, +import type { + FormFailurePayload, + InferFormData, + FormSchema, + FormSubmissionResult, + FormSuccessPayload, + SerializedFormSubmission as SerializedSubmissionState, + SerializedFormSubmission, } from './contracts' -import { createErrorBag, type InferSchemaData, type SchemaInputShape, type ValidationErrorBag, type WebFileLike } from '@holo-js/validation' +import { FormContractError } from './errors' +import { clearSensitiveInputValues, sanitizeFlashedInput } from './sensitiveInput' +import { + type FormLikeValidationInput, + createErrorBag, + type FieldBuilderInput, + type InferFieldOutput, + type SchemaInputShape, + type ValidationErrorBag, + type ValidationSchema, + type WebFileLike, + validate as validateInput, +} from '@holo-js/validation' import { getClientCsrfField } from './client-security' type PrimitiveLike = string | number | boolean | bigint | symbol | null | undefined | Date | Blob | WebFileLike @@ -23,20 +33,22 @@ export interface ClientSubmitContext { readonly formData: FormData } -export type ClientSubmitResult +export type ClientSubmitResult = FormSubmissionResult | SerializedFormSubmission | FormFailurePayload - | FormSuccessPayload + | FormSuccessPayload -export interface UseFormOptions { +export interface UseFormOptions { readonly action?: string readonly method?: string readonly csrf?: boolean readonly validateOn?: ValidateOnMode readonly initialValues?: Partial readonly initialState?: SerializedFormSubmission - readonly submitter?: (context: ClientSubmitContext) => Promise> | ClientSubmitResult + readonly submitter?: ( + context: ClientSubmitContext, + ) => Promise> | ClientSubmitResult } export interface FormFieldState { @@ -58,30 +70,44 @@ export type FormFieldTree = [TData] extends [readonly unknown[]] ? { readonly [K in keyof TData]: FormFieldTree } : FormFieldState -export interface UseFormResult { - readonly fields: FormFieldTree +type FormFieldTreeFromShape = { + readonly [K in Extract]: + TShape[K] extends FieldBuilderInput + ? FormFieldState> + : TShape[K] extends SchemaInputShape + ? FormFieldTreeFromShape + : never +} + +export type InferFormFieldTree + = TSchema extends FormSchema + ? FormFieldTreeFromShape + : never + +export interface UseFormResult> { + readonly fields: TFields readonly values: TData readonly errors: ValidationErrorBag readonly submitting: boolean readonly valid: boolean - readonly lastSubmission?: SerializedFormSubmission | FormSuccessPayload + readonly lastSubmission?: SerializedFormSubmission | FormFailurePayload | FormSuccessPayload subscribe(listener: () => void): () => void validate(): Promise> validateField(path: string): Promise - submit(): Promise> + submit(): Promise> reset(values?: Partial): void setValue(path: string, value: unknown): Promise - applyServerState(result: ClientSubmitResult): ClientSubmitResult + applyServerState(result: ClientSubmitResult): ClientSubmitResult } -type MutableState = { +type MutableState = { values: TData initialValues: TData flattenedErrors: Record touched: Set dirty: Set submitting: boolean - lastSubmission?: SerializedFormSubmission | FormSuccessPayload + lastSubmission?: SerializedFormSubmission | FormFailurePayload | FormSuccessPayload listeners: Set<() => void> } @@ -90,6 +116,135 @@ type SchemaFieldLike = { readonly definition: object } +function normalizeStatus(value: number | undefined, fallback: number): number { + if (typeof value === 'undefined') { + return fallback + } + + if (!Number.isInteger(value) || value < 100) { + throw new FormContractError('HTTP status codes must be integers greater than or equal to 100.') + } + + return value +} + +function serializeSubmissionState( + valid: boolean, + values: Partial | TData, + errors: ValidationErrorBag, +): SerializedFormSubmission { + return Object.freeze({ + valid, + submitted: true as const, + values: sanitizeFlashedInput(values), + errors: errors.flatten(), + }) +} + +function createSubmission( + valid: boolean, + values: Partial | TData, + errors: ValidationErrorBag, + failureStatus = 422, +): FormSubmissionResult { + const normalizedFailureStatus = normalizeStatus(failureStatus, 422) + const serialize = () => serializeSubmissionState(valid, values, errors) + + const failure = (): FormFailurePayload => ({ + ok: false, + status: normalizedFailureStatus, + valid: false as const, + values: sanitizeFlashedInput(values) as Partial, + errors: errors.flatten(), + }) + + const success = (payload?: TPayload, status?: number): FormSuccessPayload => ({ + ok: true, + status: normalizeStatus(status, 200), + data: payload, + }) + + if (valid) { + const data = values as TData + return Object.freeze({ + valid: true as const, + submitted: true as const, + data, + values: data, + errors, + serialize, + success, + fail(status?: number) { + const payload = failure() + return { + ...payload, + status: normalizeStatus(status, payload.status), + } + }, + }) + } + + return Object.freeze({ + valid: false as const, + submitted: true as const, + data: undefined, + values: values as Partial, + errors, + serialize, + success, + fail(status?: number) { + const payload = failure() + return { + ...payload, + status: normalizeStatus(status, payload.status), + } + }, + }) +} + +function createSuccessfulSubmission( + schemaDefinition: FormSchema, + data: TData, +): FormSubmissionResult { + void schemaDefinition + return createSubmission(true, data, createErrorBag()) +} + +function createFailedSubmission( + schemaDefinition: FormSchema, + values: Partial, + flattenedErrors: Record, + status = 422, +): FormSubmissionResult { + void schemaDefinition + return createSubmission( + false, + values, + createErrorBag(flattenedErrors), + normalizeStatus(status, 422), + ) +} + +async function validateClientValues( + values: TData, + schemaDefinition: FormSchema, +): Promise> { + const result = await validateInput( + values as unknown as FormLikeValidationInput, + schemaDefinition as ValidationSchema, + ) + + if (result.valid) { + return createSuccessfulSubmission(schemaDefinition, result.data as TData) + } + + return createFailedSubmission( + schemaDefinition, + result.values as Partial, + result.errors.flatten(), + ) +} + function isPlainObject(value: unknown): value is Record { return !!value && typeof value === 'object' @@ -290,7 +445,7 @@ function createTypedErrorBag(flattenedErrors: Record(flattenedErrors) } -function notifyListeners(state: MutableState): void { +function notifyListeners(state: MutableState): void { for (const listener of state.listeners) { listener() } @@ -312,8 +467,8 @@ function collectErrorsForPath( } function buildFieldsTree( - state: MutableState, - schemaDefinition: FormSchema, + state: MutableState, + schemaDefinition: FormSchema, source: unknown, validateOn: ValidateOnMode, prefix = '', @@ -346,9 +501,8 @@ function buildFieldsTree( } if (validateOn === 'change') { - const submission = await validate(state.values as unknown as Record, schemaDefinition as never) as FormSubmissionResult + const submission = await validateClientValues(state.values, schemaDefinition) as FormSubmissionResult state.flattenedErrors = submission.errors.flatten() - state.lastSubmission = submission.serialize() as SerializedFormSubmission } notifyListeners(state) @@ -359,15 +513,14 @@ function buildFieldsTree( async onBlur() { state.touched.add(path) if (validateOn === 'blur') { - const submission = await validate(state.values as unknown as Record, schemaDefinition as never) as FormSubmissionResult + const submission = await validateClientValues(state.values, schemaDefinition) as FormSubmissionResult state.flattenedErrors = submission.errors.flatten() - state.lastSubmission = submission.serialize() as SerializedFormSubmission } notifyListeners(state) }, async validate() { - const submission = await validate(state.values as unknown as Record, schemaDefinition as never) + const submission = await validateClientValues(state.values, schemaDefinition) state.flattenedErrors = submission.errors.flatten() notifyListeners(state) return state.flattenedErrors[path] ?? [] @@ -383,7 +536,9 @@ function buildFieldsTree( return Object.freeze(Object.fromEntries(entries)) as FormFieldTree } -function isSubmissionResult(value: ClientSubmitResult): value is FormSubmissionResult { +function isSubmissionResult( + value: ClientSubmitResult, +): value is FormSubmissionResult { return 'valid' in value && 'errors' in value && 'values' in value @@ -393,15 +548,17 @@ function isSubmissionResult(value: ClientSubmitResult): value is F && typeof value.errors.flatten === 'function' } -function isSerializedSubmission(value: ClientSubmitResult): value is SerializedSubmissionState { +function isSerializedSubmission( + value: ClientSubmitResult, +): value is SerializedSubmissionState { return 'submitted' in value && 'errors' in value && 'values' in value && !('ok' in value) } -function normalizeSubmissionLike( - schemaDefinition: FormSchema, +function normalizeSubmissionLike( + schemaDefinition: FormSchema, values: TData, - result: ClientSubmitResult, -): ClientSubmitResult { + result: ClientSubmitResult, +): ClientSubmitResult { if (isSubmissionResult(result)) { return result } @@ -415,17 +572,17 @@ function normalizeSubmissionLike( if (isSerializedSubmission(result)) { if (result.valid) { - return createSuccessfulSubmission(schemaDefinition as never, result.values as never) as FormSubmissionResult + return createSuccessfulSubmission(schemaDefinition, result.values as TData) as FormSubmissionResult } - return createFailedSubmission(schemaDefinition as never, result.values as never, result.errors) as FormSubmissionResult + return createFailedSubmission(schemaDefinition, result.values as Partial, result.errors) as FormSubmissionResult } if ('ok' in result && result.ok === true) { return result } - return createSuccessfulSubmission(schemaDefinition as never, values as never) as FormSubmissionResult + return createSuccessfulSubmission(schemaDefinition, values) as FormSubmissionResult } function appendQueryString(action: string, formData: FormData): string { @@ -447,20 +604,20 @@ function isJsonResponse(response: Response): boolean { return contentType === 'application/json' || contentType?.endsWith('+json') === true } -async function normalizeFetchResponse( +async function normalizeFetchResponse( response: Response, fallbackValues: Partial | TData, -): Promise> { +): Promise> { if (response.status === 204 || response.status === 205) { return { ok: true, status: response.status, data: undefined, - } + } as FormSuccessPayload } if (isJsonResponse(response)) { - return await response.json() as ClientSubmitResult + return await response.json() as ClientSubmitResult } if (response.ok) { @@ -468,11 +625,11 @@ async function normalizeFetchResponse( ok: true, status: response.status, data: undefined, - } + } as FormSuccessPayload } try { - return await response.json() as ClientSubmitResult + return await response.json() as ClientSubmitResult } catch { return { ok: false, @@ -484,12 +641,14 @@ async function normalizeFetchResponse( } } -async function defaultSubmitter(context: ClientSubmitContext): Promise> { +async function defaultSubmitter( + context: ClientSubmitContext, +): Promise> { if (typeof fetch !== 'function' || !context.action) { return { ok: true, status: 200, - data: context.values, + data: context.values as unknown as TSuccess, } } @@ -503,10 +662,10 @@ async function defaultSubmitter(context: ClientSubmitContext): Pro ok: true, status: response.status, data: undefined, - } + } as FormSuccessPayload } - return await normalizeFetchResponse(response, context.values) + return await normalizeFetchResponse(response, context.values) } const response = await fetch(context.action, { @@ -514,7 +673,7 @@ async function defaultSubmitter(context: ClientSubmitContext): Pro body: context.formData, }) - return await normalizeFetchResponse(response, context.values) + return await normalizeFetchResponse(response, context.values) } function isSafeMethod(method: string): boolean { @@ -525,18 +684,18 @@ function isSafeMethod(method: string): boolean { || normalized === 'TRACE' } -export function useForm>( +export function useForm( schemaDefinition: TSchema, - options: UseFormOptions> = {}, -): UseFormResult> { - type TData = InferSchemaData + options: UseFormOptions, TSuccess> = {}, +): UseFormResult, TSuccess, InferFormFieldTree> { + type TData = InferFormData const initialValues = mergeValues( normalizeObject(options.initialState?.values), options.initialValues, ) - const state: MutableState = { + const state: MutableState = { values: cloneValue(initialValues), initialValues: cloneValue(initialValues), flattenedErrors: { ...(options.initialState?.errors ?? {}) }, @@ -551,15 +710,14 @@ export function useForm>( const fieldPaths = flattenLeafPaths(schemaDefinition.fields) const fields = buildFieldsTree( state, - schemaDefinition as unknown as FormSchema, + schemaDefinition, schemaDefinition.fields, validateOn, - ) as FormFieldTree + ) as unknown as InferFormFieldTree async function runValidation(): Promise> { - const submission = await validate(state.values as unknown as Record, schemaDefinition as never) as FormSubmissionResult + const submission = await validateClientValues(state.values, schemaDefinition) as FormSubmissionResult state.flattenedErrors = submission.errors.flatten() - state.lastSubmission = submission.serialize() notifyListeners(state) return submission } @@ -603,7 +761,7 @@ export function useForm>( return local } - const submitter = options.submitter ?? defaultSubmitter + const submitter = options.submitter ?? defaultSubmitter const method = options.method ?? 'POST' const formData = buildFormData(state.values) @@ -652,9 +810,9 @@ export function useForm>( notifyListeners(state) }, - applyServerState(result: ClientSubmitResult) { + applyServerState(result: ClientSubmitResult) { const normalized = normalizeSubmissionLike( - schemaDefinition as unknown as FormSchema, + schemaDefinition, state.values, result, ) @@ -668,24 +826,23 @@ export function useForm>( if ('ok' in normalized && normalized.ok === false) { state.values = mergeValues(state.values, normalized.values) + clearSensitiveInputValues(state.values) state.flattenedErrors = normalized.errors - state.lastSubmission = { - valid: false, - submitted: true, - values: normalized.values, - errors: normalized.errors, - } + state.lastSubmission = normalized notifyListeners(state) return normalized } const normalizedSubmission = normalized as FormSubmissionResult state.values = mergeValues(state.values, normalizedSubmission.values) + clearSensitiveInputValues(state.values) state.flattenedErrors = normalizedSubmission.errors.flatten() - state.lastSubmission = normalizedSubmission.serialize() + state.lastSubmission = normalizedSubmission.valid + ? undefined + : normalizedSubmission.fail() notifyListeners(state) return normalizedSubmission }, - }) satisfies UseFormResult + }) satisfies UseFormResult> } diff --git a/packages/forms/src/contracts.ts b/packages/forms/src/contracts.ts index 2f0f225..d31b627 100644 --- a/packages/forms/src/contracts.ts +++ b/packages/forms/src/contracts.ts @@ -5,17 +5,15 @@ import { type ValidationErrorBag, type ValidationSchema, createErrorBag, - defineSchema, - isValidationSchema, validate as validateInput, } from '@holo-js/validation' -import { formsSecurityInternals, loadSecurityModule } from './security' +import { FormContractError } from './errors' +import type { FormSchema } from './schema' +import { sanitizeFlashedInput } from './sensitiveInput' -export interface FormSchema extends ValidationSchema { - readonly mode: 'form' - readonly fields: ValidationSchema['fields'] - readonly $values?: Partial> -} +export { FormContractError } from './errors' +export { type FormSchema, type InferFormData, isFormSchema, schema } from './schema' +export { sanitizeFlashedInput } from './sensitiveInput' export interface FormFailurePayload { readonly ok: false @@ -32,6 +30,7 @@ export interface FormSuccessPayload { } export interface SerializedFormSubmission { + readonly ok?: false readonly valid: boolean readonly submitted: true readonly values: Partial | TData @@ -103,31 +102,6 @@ export interface FormSubmissionFailure { export type FormSubmissionResult = FormSubmissionSuccess | FormSubmissionFailure -export class FormContractError extends Error { - constructor(message: string) { - super(message) - this.name = 'FormContractError' - } -} - -export function schema( - shapeOrSchema: TShape | ValidationSchema, -): FormSchema { - const base = isValidationSchema(shapeOrSchema) - ? shapeOrSchema - : defineSchema(shapeOrSchema) - - return Object.freeze({ - ...base, - mode: 'form' as const, - }) as FormSchema -} - -export function isFormSchema(value: unknown): value is FormSchema { - return isValidationSchema(value) - && (value as { mode?: unknown }).mode === 'form' -} - function normalizeStatus(value: number | undefined, fallback: number): number { if (typeof value === 'undefined') { return fallback @@ -148,7 +122,7 @@ function serializeSubmissionState( return Object.freeze({ valid, submitted: true as const, - values, + values: sanitizeFlashedInput(values), errors: errors.flatten(), }) } @@ -166,7 +140,7 @@ function createSubmission( ok: false, status: normalizedFailureStatus, valid: false as const, - values: values as Partial, + values: sanitizeFlashedInput(values) as Partial, errors: errors.flatten(), }) @@ -375,21 +349,63 @@ function extractRequestLikeBody( return String(rawBody) } +function isStructuredRequestLikeObject(value: unknown): value is { + readonly method?: string + readonly path?: string + readonly url?: string | URL + readonly headers?: RequestLikeHeaders + readonly body?: unknown +} { + if (!value || typeof value !== 'object') { + return false + } + + const candidate = value as { + readonly method?: unknown + readonly path?: unknown + readonly url?: unknown + readonly headers?: unknown + readonly body?: unknown + } + + return typeof candidate.method === 'string' + || typeof candidate.path === 'string' + || typeof candidate.url === 'string' + || candidate.url instanceof URL + || isRequestLikeHeaders(candidate.headers) + || typeof candidate.body !== 'undefined' +} + function isRequestLikeInput(input: unknown): input is FormRequestLikeInput { if (!input || typeof input !== 'object') { return false } const candidate = input as FormRequestLikeInput - return candidate.web?.request instanceof Request - || candidate.req instanceof Request - || typeof candidate.method === 'string' + if (candidate.web?.request instanceof Request || candidate.req instanceof Request) { + return true + } + + if (isStructuredRequestLikeObject(candidate.web?.request)) { + return true + } + + if (isStructuredRequestLikeObject(candidate.req)) { + return true + } + + if (isStructuredRequestLikeObject(candidate.node?.req)) { + return true + } + + const hasRequestMetadata = typeof candidate.method === 'string' || typeof candidate.path === 'string' || typeof candidate.url === 'string' || candidate.url instanceof URL - || isRequestLikeHeaders(candidate.headers) - || (!!candidate.req && typeof candidate.req === 'object') - || (!!candidate.node?.req && typeof candidate.node.req === 'object') + const hasStructuredHeaders = isRequestLikeHeaders(candidate.headers) + const hasBody = typeof candidate.body !== 'undefined' + + return hasRequestMetadata && (hasStructuredHeaders || hasBody) } function normalizeRequestLikeInput(input: FormLikeValidationInput | FormRequestLikeInput | null | undefined): Request | undefined { @@ -473,9 +489,7 @@ export async function validate( | FormSubmissionResult> | undefined const usesSecurityOptions = options.csrf === true || typeof options.throttle === 'string' - const normalizedRequestInput = usesSecurityOptions - ? normalizeRequestLikeInput(input) - : undefined + const normalizedRequestInput = normalizeRequestLikeInput(input) const validationInput = normalizedRequestInput ?? input if (usesSecurityOptions && !normalizedRequestInput) { @@ -488,6 +502,7 @@ export async function validate( const request = normalizedRequestInput as Request try { + const { loadSecurityModule } = await import('./security') const security = await loadSecurityModule() const verificationRequest = (() => { try { @@ -513,6 +528,7 @@ export async function validate( }) } } catch (error) { + const { formsSecurityInternals } = await import('./security') if (formsSecurityInternals.isRootSecurityError(error)) { if (validatedSubmission) { return createFailedSubmission( @@ -561,5 +577,6 @@ export const formsInternals = { normalizeRequestLikeInput, normalizeStatus, normalizeRequestHeaders, + sanitizeFlashedInput, serializeSubmissionState, } diff --git a/packages/forms/src/errors.ts b/packages/forms/src/errors.ts new file mode 100644 index 0000000..0096986 --- /dev/null +++ b/packages/forms/src/errors.ts @@ -0,0 +1,6 @@ +export class FormContractError extends Error { + constructor(message: string) { + super(message) + this.name = 'FormContractError' + } +} diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts index 1bb0103..4533ac0 100644 --- a/packages/forms/src/index.ts +++ b/packages/forms/src/index.ts @@ -2,13 +2,18 @@ export { FormContractError, createFailedSubmission, createSuccessfulSubmission, - schema, formsInternals, - isFormSchema, + sanitizeFlashedInput, validate, } from './contracts' +export { sensitiveInputInternals } from './sensitiveInput' +export { + schema, + isFormSchema, +} from './schema' export type { FormFailurePayload, + InferFormData, FormRequestLikeInput, FormSchema, FormSecurityOptions, @@ -24,7 +29,7 @@ export { field, parse, safeParse, -} from '@holo-js/validation' +} from './schema' export type { ErrorTree, ErrorTreeNode, @@ -41,4 +46,4 @@ export type { ValidationSchema, ValidationSuccess, WebFileLike, -} from '@holo-js/validation' +} from './schema' diff --git a/packages/forms/src/schema.ts b/packages/forms/src/schema.ts new file mode 100644 index 0000000..438a6e3 --- /dev/null +++ b/packages/forms/src/schema.ts @@ -0,0 +1,62 @@ +import { + type InferSchemaData, + type SchemaInputShape, + type ValidationSchema, + defineSchema, + isValidationSchema, +} from '@holo-js/validation' + +export interface FormSchema extends ValidationSchema { + readonly mode: 'form' + readonly fields: ValidationSchema['fields'] + readonly $values?: Partial> +} + +export type InferFormData + = TSchema extends FormSchema + ? InferSchemaData + : never + +export function schema( + shapeOrSchema: TShape | ValidationSchema, +): FormSchema { + const base = isValidationSchema(shapeOrSchema) + ? shapeOrSchema + : defineSchema(shapeOrSchema) + + return Object.freeze({ + ...base, + mode: 'form' as const, + }) as FormSchema +} + +export function isFormSchema(value: unknown): value is FormSchema { + return isValidationSchema(value) + && (value as { mode?: unknown }).mode === 'form' +} + +export { + createErrorBag, + defineSchema, + field, + parse, + safeParse, +} from '@holo-js/validation' + +export type { + ErrorTree, + ErrorTreeNode, + FieldDefinition, + FieldRule, + InferSchemaData, + StandardSchemaV1, + StandardSchemaV1Issue, + StandardSchemaV1Props, + StandardSchemaV1Result, + ValidationErrorBag, + ValidationFailure, + ValidationResult, + ValidationSchema, + ValidationSuccess, + WebFileLike, +} from '@holo-js/validation' diff --git a/packages/forms/src/security-shared.ts b/packages/forms/src/security-shared.ts new file mode 100644 index 0000000..d3291a9 --- /dev/null +++ b/packages/forms/src/security-shared.ts @@ -0,0 +1,64 @@ +import { FormContractError } from './errors' + +export function isMissingOptionalPackageError(error: unknown): boolean { + if (!(error instanceof Error)) { + return false + } + + return [ + /Cannot find package ['"]?@holo-js\/security['"]?/, + /Cannot find module ['"]?@holo-js\/security['"]?/, + /Failed to resolve module specifier ['"]?@holo-js\/security['"]?/, + /Failed to load url ['"]?@holo-js\/security['"]?/, + /Could not resolve ['"]?@holo-js\/security['"]?/, + ].some(pattern => pattern.test(error.message)) +} + +export function createMissingSecurityPackageError(): FormContractError { + return new FormContractError( + '[@holo-js/forms] Security-aware form options require the optional @holo-js/security package to be installed.', + ) +} + +export function parseCookieHeader(header: string): Readonly> { + const decodeCookiePart = (value: string): string | undefined => { + try { + return decodeURIComponent(value) + } catch { + return undefined + } + } + + return Object.freeze(Object.fromEntries( + header + .split(';') + .map(segment => segment.trim()) + .filter(Boolean) + .map((segment) => { + const separator = segment.indexOf('=') + if (separator <= 0) { + return undefined + } + + const key = decodeCookiePart(segment.slice(0, separator)) + const value = decodeCookiePart(segment.slice(separator + 1)) + if (!key || typeof value === 'undefined') { + return undefined + } + + return [ + key, + value, + ] as const + }) + .filter((entry): entry is readonly [string, string] => !!entry), + )) +} + +export function isRootSecurityError(error: unknown): error is Error & { readonly status: number } { + const candidate = error as { status?: unknown } | undefined + + return error instanceof Error + && typeof candidate?.status === 'number' + && (candidate.status === 419 || candidate.status === 429) +} diff --git a/packages/forms/src/security.ts b/packages/forms/src/security.ts index e5e928e..173fad1 100644 --- a/packages/forms/src/security.ts +++ b/packages/forms/src/security.ts @@ -1,4 +1,9 @@ -import { FormContractError } from './contracts' +import { + createMissingSecurityPackageError, + isMissingOptionalPackageError, + isRootSecurityError, + parseCookieHeader, +} from './security-shared' type SecurityModule = { csrf: { @@ -14,34 +19,12 @@ type BrowserLikeGlobal = typeof globalThis & { __holoFormsSecurityImport__?: () => Promise } -function isMissingOptionalPackageError(error: unknown): boolean { - if (!(error instanceof Error)) { - return false - } - - return [ - /Cannot find package ['"]?@holo-js\/security['"]?/, - /Cannot find module ['"]?@holo-js\/security['"]?/, - /Failed to resolve module specifier ['"]?@holo-js\/security['"]?/, - /Failed to load url ['"]?@holo-js\/security['"]?/, - /Could not resolve ['"]?@holo-js\/security['"]?/, - ].some(pattern => pattern.test(error.message)) -} - -function createMissingSecurityPackageError(): FormContractError { - return new FormContractError( - '[@holo-js/forms] Security-aware form options require the optional @holo-js/security package to be installed.', - ) -} - async function importSecurityModule(): Promise { - const specifier = ['@holo-js', 'security'].join('/') as string - if (typeof process !== 'undefined' && process.env && process.env.VITEST) { - return await import(/* @vite-ignore */ specifier) + return await import(/* @vite-ignore */ '@holo-js/security') } - return await import(specifier) + return await import('@holo-js/security') } export async function loadSecurityModule(): Promise { @@ -71,49 +54,6 @@ export function resetSecurityModuleCache(): void { securityModulePromise = undefined } -function parseCookieHeader(header: string): Readonly> { - const decodeCookiePart = (value: string): string | undefined => { - try { - return decodeURIComponent(value) - } catch { - return undefined - } - } - - return Object.freeze(Object.fromEntries( - header - .split(';') - .map(segment => segment.trim()) - .filter(Boolean) - .map((segment) => { - const separator = segment.indexOf('=') - if (separator <= 0) { - return undefined - } - - const key = decodeCookiePart(segment.slice(0, separator)) - const value = decodeCookiePart(segment.slice(separator + 1)) - if (!key || typeof value === 'undefined') { - return undefined - } - - return [ - key, - value, - ] as const - }) - .filter((entry): entry is readonly [string, string] => !!entry), - )) -} - -function isRootSecurityError(error: unknown): error is Error & { readonly status: number } { - const candidate = error as { status?: unknown } | undefined - - return error instanceof Error - && typeof candidate?.status === 'number' - && (candidate.status === 419 || candidate.status === 429) -} - export const formsSecurityInternals = { createMissingSecurityPackageError, isMissingOptionalPackageError, diff --git a/packages/forms/src/sensitiveInput.ts b/packages/forms/src/sensitiveInput.ts new file mode 100644 index 0000000..2603e3c --- /dev/null +++ b/packages/forms/src/sensitiveInput.ts @@ -0,0 +1,54 @@ +const DEFAULT_DONT_FLASH_FIELDS = Object.freeze([ + 'confirm_password', + 'confirmPassword', + 'current_password', + 'currentPassword', + 'new_password', + 'newPassword', + 'password', + 'password_confirmation', + 'passwordConfirmation', + 'token', + 'verification_code', + 'verificationCode', + 'verification_token', + 'verificationToken', +]) + +const DEFAULT_DONT_FLASH_FIELD_SET = new Set(DEFAULT_DONT_FLASH_FIELDS) + +function isPlainObject(value: unknown): value is Record { + return !!value + && typeof value === 'object' + && !Array.isArray(value) + && !(value instanceof Date) + && !(value instanceof Blob) +} + +export function sanitizeFlashedInput( + values: Partial | TData, +): Partial | TData { + if (!isPlainObject(values)) { + return values + } + + return Object.fromEntries( + Object.entries(values).filter(([key]) => !DEFAULT_DONT_FLASH_FIELD_SET.has(key)), + ) as Partial | TData +} + +export function clearSensitiveInputValues(values: TData): TData { + if (!isPlainObject(values)) { + return values + } + + for (const field of DEFAULT_DONT_FLASH_FIELD_SET) { + delete values[field] + } + + return values +} + +export const sensitiveInputInternals = { + DEFAULT_DONT_FLASH_FIELDS, +} diff --git a/packages/forms/tests/client.test.ts b/packages/forms/tests/client.test.ts index bd4636e..1536722 100644 --- a/packages/forms/tests/client.test.ts +++ b/packages/forms/tests/client.test.ts @@ -297,16 +297,14 @@ describe('@holo-js/forms client', () => { const validated = await client.validate() expect(validated.valid).toBe(false) - expect(client.lastSubmission).toEqual(validated.serialize()) + expect(client.lastSubmission).toBeUndefined() const fieldErrors = await client.validateField('email') expect(fieldErrors.length).toBeGreaterThan(0) const failure = await client.submit() expect('valid' in failure && failure.valid === false).toBe(true) - expect(client.lastSubmission).toEqual( - 'serialize' in failure ? failure.serialize() : client.lastSubmission, - ) + expect(client.lastSubmission).toBeUndefined() client.reset({ email: 'reset@example.com', @@ -467,6 +465,40 @@ describe('@holo-js/forms client', () => { expect('valid' in fallback && fallback.valid === true).toBe(true) }) + it('clears Laravel-style dontFlash fields after server-side failures', () => { + const registerUser = schema({ + email: field.string().required().email(), + password: field.string().required().min(8), + passwordConfirmation: field.string().required(), + }) + + const client = useForm(registerUser, { + initialValues: { + email: 'ava@example.com', + password: 'super-secret', + passwordConfirmation: 'super-secret', + }, + }) + + client.applyServerState({ + ok: false, + status: 422, + valid: false, + values: { + email: 'bad', + }, + errors: { + email: ['Email must be valid.'], + password: ['The password field is required.'], + }, + }) + + expect(client.values.email).toBe('bad') + expect(client.values.password).toBeUndefined() + expect(client.values.passwordConfirmation).toBeUndefined() + expect(client.errors.first('password')).toBe('The password field is required.') + }) + it('submits through custom submitters and default transports', async () => { const registerUser = schema({ email: field.string().required().email(), @@ -646,8 +678,10 @@ describe('@holo-js/forms client', () => { errors: {}, }) expect(nonJsonFailureClient.lastSubmission).toEqual({ - valid: false, + ok: false, + status: 500, submitted: true, + valid: false, values: { email: 'ava@example.com', publishedAt: new Date('2026-04-04T10:00:00.000Z'), @@ -709,7 +743,7 @@ describe('@holo-js/forms client', () => { const result = await client.submit() expect('ok' in result && result.ok === false).toBe(true) - if ('ok' in result && result.ok === false) { + if ('ok' in result && result.ok === false && 'status' in result) { expect(result.status).toBe(409) expect(result.errors.email).toEqual(['Email is already taken.']) } diff --git a/packages/forms/tests/client.type.test.ts b/packages/forms/tests/client.type.test.ts index bf53031..178f1aa 100644 --- a/packages/forms/tests/client.type.test.ts +++ b/packages/forms/tests/client.type.test.ts @@ -82,4 +82,42 @@ describe('@holo-js/forms client typing', () => { void invalidField void (0 as unknown as ClientAssertion) }) + + it('keeps schema-driven field inference for forms that include a name field', () => { + const registerUser = schema({ + name: field.string().required(), + email: field.string().required().email(), + password: field.string().required().min(8).confirmed(), + passwordConfirmation: field.string().required(), + }) + + const client = useForm(registerUser, { + initialValues: { + name: 'Ava', + email: 'ava@example.com', + password: 'supersecret', + passwordConfirmation: 'supersecret', + }, + submitter() { + return { + ok: true as const, + status: 200, + data: { + message: 'Account created.', + }, + } + }, + }) + + const nameField: FormFieldState = client.fields.name + const confirmationField: FormFieldState = client.fields.passwordConfirmation + + if (client.lastSubmission?.ok === true) { + const message: string = client.lastSubmission.data.message + void message + } + + void nameField + void confirmationField + }) }) diff --git a/packages/forms/tests/contracts.test.ts b/packages/forms/tests/contracts.test.ts index ef12112..bcdf9e1 100644 --- a/packages/forms/tests/contracts.test.ts +++ b/packages/forms/tests/contracts.test.ts @@ -228,6 +228,55 @@ describe('@holo-js/forms contracts', () => { }) }) + it('excludes Laravel-style dontFlash fields from serialized failure payloads', async () => { + const registerUser = schema({ + email: field.string().required().email(), + password: field.string().required().min(8), + passwordConfirmation: field.string().required(), + token: field.string().required(), + }) + + const failure = await validate({ + email: 'bad', + password: 'super-secret', + passwordConfirmation: 'super-secret', + token: 'reset-token', + }, registerUser) + + expect(failure.valid).toBe(false) + if (failure.valid) { + throw new Error('Expected form submission failure.') + } + + expect(failure.values).toEqual({ + email: 'bad', + password: 'super-secret', + passwordConfirmation: 'super-secret', + token: 'reset-token', + }) + expect(failure.serialize()).toEqual({ + valid: false, + submitted: true, + values: { + email: 'bad', + }, + errors: { + email: ['Invalid email: Received "bad"'], + }, + }) + expect(failure.fail()).toEqual({ + ok: false, + status: 422, + valid: false, + values: { + email: 'bad', + }, + errors: { + email: ['Invalid email: Received "bad"'], + }, + }) + }) + it('does not coerce plain form objects with request-like field names into Request inputs', async () => { const requestMeta = schema({ method: field.string().required(), @@ -256,6 +305,33 @@ describe('@holo-js/forms contracts', () => { }) }) + it('normalizes request-like event inputs even without security options', async () => { + const forgotPassword = schema({ + email: field.string().required().email(), + }) + + const submission = await validate({ + method: 'POST', + path: '/forgot-password', + headers: { + host: 'app.test', + 'content-type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + email: 'ava@example.com', + }), + }, forgotPassword) + + expect(submission.valid).toBe(true) + if (!submission.valid) { + throw new Error('Expected request-like event validation success.') + } + + expect(submission.data).toEqual({ + email: 'ava@example.com', + }) + }) + it('runs csrf and throttle checks through validate() and returns form-shaped security failures', async () => { const login = schema({ email: field.string().required().email(), @@ -482,6 +558,28 @@ describe('@holo-js/forms contracts', () => { })).toBe(nodeRequest) }) + it('does not treat arbitrary nested request containers as requests', () => { + expect(formsInternals.normalizeRequestLikeInput({ + req: { + email: 'ava@example.com', + }, + })).toBeUndefined() + expect(formsInternals.normalizeRequestLikeInput({ + web: { + request: { + email: 'ava@example.com', + }, + }, + })).toBeUndefined() + expect(formsInternals.normalizeRequestLikeInput({ + node: { + req: { + email: 'ava@example.com', + }, + }, + })).toBeUndefined() + }) + it('marks streamed request-like bodies as duplex requests', async () => { const body = { async *[Symbol.asyncIterator]() { diff --git a/packages/forms/tests/docs-examples.test.ts b/packages/forms/tests/docs-examples.test.ts index 3fe392d..23cfefe 100644 --- a/packages/forms/tests/docs-examples.test.ts +++ b/packages/forms/tests/docs-examples.test.ts @@ -33,7 +33,10 @@ describe('@holo-js/forms documented examples', () => { ok: false, status: 422, valid: false, - values: registerFailure.values, + values: { + email: 'bad', + name: 'Ava', + }, errors: registerFailure.errors.flatten(), }) } diff --git a/packages/forms/tests/security.test.ts b/packages/forms/tests/security.test.ts index 17f573f..23a2009 100644 --- a/packages/forms/tests/security.test.ts +++ b/packages/forms/tests/security.test.ts @@ -11,7 +11,9 @@ const formsSourceRoot = resolve(import.meta.dirname, '..') const formsFixtureFiles = [ 'src/client-security.ts', 'src/contracts.ts', + 'src/errors.ts', 'src/security.ts', + 'src/security-shared.ts', ] as const const tempDirs: string[] = [] diff --git a/packages/forms/tsup.config.ts b/packages/forms/tsup.config.ts index 11e5310..ab7b471 100644 --- a/packages/forms/tsup.config.ts +++ b/packages/forms/tsup.config.ts @@ -5,6 +5,7 @@ const outDir = process.env.HOLO_BUILD_OUT_DIR ?? 'dist' export default defineConfig({ entry: { index: 'src/index.ts', + schema: 'src/schema.ts', client: 'src/client.ts', }, format: ['esm'], diff --git a/packages/mail/src/runtime.ts b/packages/mail/src/runtime.ts index aed5189..7150fe0 100644 --- a/packages/mail/src/runtime.ts +++ b/packages/mail/src/runtime.ts @@ -862,10 +862,10 @@ async function persistPreviewArtifact(artifact: MailPreviewArtifact): Promise): Readonly> | undefined { @@ -909,20 +909,20 @@ async function createSmtpAttachment( } satisfies Omit if (typeof attachment.path === 'string') { - return Object.freeze({ + return { ...base, path: attachment.path, - }) + } } if (attachment.storage) { const storageModule = await loadStorageModule() const disk = storageModule.Storage.disk(attachment.storage.disk) if (disk.driver !== 's3') { - return Object.freeze({ + return { ...base, path: disk.path(attachment.storage.path), - }) + } } const bytes = await disk.getBytes(attachment.storage.path) @@ -933,17 +933,17 @@ async function createSmtpAttachment( ) } - return Object.freeze({ + return { ...base, content: bytes, - }) + } } if (typeof attachment.content !== 'undefined') { - return Object.freeze({ + return { ...base, content: attachment.content, - }) + } } throw new MailError( @@ -957,23 +957,23 @@ async function createSmtpMessage( context: Readonly, ): Promise { const attachments = mail.attachments.length > 0 - ? Object.freeze(await Promise.all(mail.attachments.map(createSmtpAttachment))) + ? await Promise.all(mail.attachments.map(createSmtpAttachment)) : undefined const headers = createSmtpHeaders(mail) - return Object.freeze({ + return { messageId: context.messageId, from: createSmtpAddress(mail.from), replyTo: createSmtpAddress(mail.replyTo), - to: Object.freeze(mail.to.map(createSmtpAddress)), - ...(mail.cc.length > 0 ? { cc: Object.freeze(mail.cc.map(createSmtpAddress)) } : {}), - ...(mail.bcc.length > 0 ? { bcc: Object.freeze(mail.bcc.map(createSmtpAddress)) } : {}), + to: mail.to.map(createSmtpAddress), + ...(mail.cc.length > 0 ? { cc: mail.cc.map(createSmtpAddress) } : {}), + ...(mail.bcc.length > 0 ? { bcc: mail.bcc.map(createSmtpAddress) } : {}), subject: mail.subject, ...(typeof mail.html === 'string' ? { html: mail.html } : {}), ...(typeof mail.text === 'string' ? { text: mail.text } : {}), ...(attachments ? { attachments } : {}), ...(headers ? { headers } : {}), - }) + } } async function sendViaSmtp( diff --git a/packages/mail/tests/docs-smoke.test.ts b/packages/mail/tests/docs-smoke.test.ts index 7ddf935..d6fd0f2 100644 --- a/packages/mail/tests/docs-smoke.test.ts +++ b/packages/mail/tests/docs-smoke.test.ts @@ -39,8 +39,13 @@ describe('mail documentation smoke checks', () => { expect(setup).toContain('custom `renderView` runtime binding') expect(installation).toContain('mail') expect(installation).toContain('--package forms,validation,mail') - expect(verification).toContain('npx holo install mail') + expect(verification).toContain('@holo-js/mail') + expect(verification).toContain('APP_URL') + expect(verification).toContain('AUTH_EMAIL_VERIFICATION_ROUTE') + expect(verification).toContain('generated email link') expect(reset).toContain('@holo-js/mail') + expect(reset).toContain('APP_URL') + expect(reset).toContain('AUTH_PASSWORD_RESET_ROUTE') }) it('covers final mail authoring, preview, and attachment APIs', async () => { diff --git a/packages/mail/tests/runtime.test.ts b/packages/mail/tests/runtime.test.ts index 7d98987..56604d1 100644 --- a/packages/mail/tests/runtime.test.ts +++ b/packages/mail/tests/runtime.test.ts @@ -131,12 +131,15 @@ function createQueueModuleStub(options: { readonly autoRun?: boolean } = {}) { function createNodemailerModuleStub(options: { readonly result?: { readonly messageId?: string, readonly response?: string } readonly error?: Error + readonly inspectMessage?: (message: unknown) => void } = {}) { const sendMail = vi.fn(async (message: unknown) => { if (options.error) { throw options.error } + options.inspectMessage?.(message) + return options.result ?? { messageId: 'smtp-provider-id', response: '250 accepted', @@ -1753,6 +1756,43 @@ describe('@holo-js/mail runtime', () => { response: '250 queued', }, }) + const nodemailerMutatingAddresses = createNodemailerModuleStub({ + inspectMessage(message) { + if (!message || typeof message !== 'object') { + throw new Error('Expected SMTP message object.') + } + + const smtpMessage = message as { + from?: { address?: string } + to?: Array<{ address?: string }> + } + if (!smtpMessage.from || !Array.isArray(smtpMessage.to) || !smtpMessage.to[0]) { + throw new Error('Expected SMTP message addresses.') + } + + smtpMessage.from.address = smtpMessage.from.address?.toLowerCase() + smtpMessage.to[0].address = smtpMessage.to[0].address?.toLowerCase() + }, + }) + mailRuntimeInternals.setNodemailerModuleLoader(async () => nodemailerMutatingAddresses.module) + await expect(mailRuntimeInternals.sendViaSmtp( + { + ...smtpResolvedMail, + headers: {}, + tags: [], + attachments: [], + } as never, + mailRuntimeInternals.createSendContext('mail-3c', { + mailer: 'smtp', + driver: 'smtp', + implementation: getBuiltInDriver('smtp'), + }, false), + )).resolves.toMatchObject({ + providerMessageId: 'smtp-provider-id', + provider: { + response: '250 accepted', + }, + }) mailRuntimeInternals.setNodemailerModuleLoader(undefined) const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) diff --git a/packages/notifications/tests/docs-smoke.test.ts b/packages/notifications/tests/docs-smoke.test.ts index 771f698..76ac373 100644 --- a/packages/notifications/tests/docs-smoke.test.ts +++ b/packages/notifications/tests/docs-smoke.test.ts @@ -37,9 +37,14 @@ describe('notifications documentation smoke checks', () => { expect(installation).toContain('notifications') expect(installation).toContain('--package forms,validation,notifications') expect(verification).toContain('@holo-js/notifications') - expect(verification).toContain('notify(created, verificationCreated(token))') - expect(reset).toContain('notifyUsing()') - expect(reset).toContain('auth.password-reset') + expect(verification).toContain('APP_URL') + expect(verification).toContain('AUTH_EMAIL_VERIFICATION_ROUTE') + expect(verification).toContain('generated email link') + expect(verification).not.toContain('notify(created, verificationCreated(token))') + expect(reset).toContain('@holo-js/notifications') + expect(reset).toContain('APP_URL') + expect(reset).toContain('AUTH_PASSWORD_RESET_ROUTE') + expect(reset).not.toContain('notifyUsing()') }) it('covers the final fluent API and custom channel story', async () => { diff --git a/packages/security/src/client.ts b/packages/security/src/client.ts index c4a6e14..cf1be93 100644 --- a/packages/security/src/client.ts +++ b/packages/security/src/client.ts @@ -1,4 +1,3 @@ -import { normalizeSecurityConfig } from '@holo-js/config' import type { SecurityClientBindings, SecurityClientConfig } from './contracts' export type { @@ -10,11 +9,10 @@ type RuntimeSecurityClientState = { bindings?: SecurityClientConfig } -const DEFAULT_SECURITY_CONFIG = normalizeSecurityConfig({}) const DEFAULT_SECURITY_CLIENT_CONFIG: SecurityClientConfig = Object.freeze({ csrf: Object.freeze({ - field: DEFAULT_SECURITY_CONFIG.csrf.field, - cookie: DEFAULT_SECURITY_CONFIG.csrf.cookie, + field: '_token', + cookie: 'XSRF-TOKEN', }), }) diff --git a/packages/validation/src/contracts-types.ts b/packages/validation/src/contracts-types.ts index ee6828d..c4defa8 100644 --- a/packages/validation/src/contracts-types.ts +++ b/packages/validation/src/contracts-types.ts @@ -111,7 +111,7 @@ export type NormalizeFieldInput = TInput extends ValidationFieldBuilderL export type InferFieldOutput = NormalizeFieldInput extends ValidationField ? TOutput : never export type NormalizedSchemaShape = { - readonly [K in keyof TShape]: + readonly [K in Extract]: TShape[K] extends FieldBuilderInput ? NormalizeFieldInput : TShape[K] extends SchemaInputShape @@ -126,7 +126,7 @@ export type SchemaInputShape = { export type Simplify = { -readonly [K in keyof TValue]: TValue[K] } & {} export type InferSchemaData = Simplify<{ - [K in keyof TShape]: + [K in Extract]: TShape[K] extends FieldBuilderInput ? InferFieldOutput : TShape[K] extends SchemaInputShape diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 9ee93c5..6571e92 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -22,6 +22,7 @@ export type { FieldKind, FieldRule, FormLikeValidationInput, + InferFieldOutput, InferSchemaData, PrimitiveFieldKind, SchemaInputShape, diff --git a/tests/example-app-auth-flow.mjs b/tests/example-app-auth-flow.mjs new file mode 100644 index 0000000..7bfe04d --- /dev/null +++ b/tests/example-app-auth-flow.mjs @@ -0,0 +1,325 @@ +import assert from 'node:assert/strict' + +function createCookieJar() { + const cookies = new Map() + + return { + apply(headers) { + if (cookies.size === 0) { + return + } + + headers.set('cookie', [...cookies.entries()].map(([name, value]) => `${name}=${value}`).join('; ')) + }, + capture(response) { + const setCookies = typeof response.headers.getSetCookie === 'function' + ? response.headers.getSetCookie() + : ( + response.headers.get('set-cookie') + ? [response.headers.get('set-cookie')] + : [] + ) + + for (const setCookie of setCookies) { + const firstSegment = setCookie.split(';', 1)[0] ?? '' + const equalsIndex = firstSegment.indexOf('=') + if (equalsIndex <= 0) { + continue + } + + const name = firstSegment.slice(0, equalsIndex).trim() + const value = firstSegment.slice(equalsIndex + 1).trim() + + if (value.length === 0) { + cookies.delete(name) + continue + } + + cookies.set(name, value) + } + }, + } +} + +function listSetCookieHeaders(response) { + if (typeof response.headers.getSetCookie === 'function') { + return response.headers.getSetCookie() + } + + const fallback = response.headers.get('set-cookie') + return fallback ? [fallback] : [] +} + +async function fetchText(baseUrl, path, options = {}) { + const headers = new Headers(options.headers ?? {}) + options.jar?.apply(headers) + + const response = await fetch(new URL(path, baseUrl), { + method: options.method ?? 'GET', + headers, + body: options.body, + redirect: 'manual', + }) + + options.jar?.capture(response) + + const text = await response.text() + if ((response.status < 200 || response.status >= 300) && options.allowFailure !== true) { + throw new Error(`Unexpected status ${response.status} for ${path}: ${text}`) + } + + return { + response, + text, + } +} + +async function fetchJson(baseUrl, path, options = {}) { + const headers = new Headers(options.headers ?? {}) + let body = options.body + + if (options.fields) { + const payload = new FormData() + for (const [key, value] of Object.entries(options.fields)) { + if (typeof value === 'undefined' || value === null) { + continue + } + + payload.set(key, String(value)) + } + + body = payload + } + + const result = await fetchText(baseUrl, path, { + method: options.method ?? (options.fields ? 'POST' : 'GET'), + headers, + body, + jar: options.jar, + allowFailure: options.allowFailure, + }) + + try { + return { + response: result.response, + json: JSON.parse(result.text), + } + } catch (error) { + throw new Error(`Expected JSON from ${path}: ${error instanceof Error ? error.message : String(error)}\n${result.text}`) + } +} + +async function waitForOutputMatch(getOutput, matcher, startIndex = 0, timeoutMs = 10000) { + const startedAt = Date.now() + + while (Date.now() - startedAt < timeoutMs) { + const output = getOutput().slice(startIndex) + const match = output.match(matcher) + if (match) { + return match + } + + await new Promise(resolve => setTimeout(resolve, 100)) + } + + throw new Error(`Timed out waiting for auth mail output to match ${matcher}`) +} + +const authTokenPattern = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[A-Za-z0-9_-]+)/i + +export async function assertExampleAppAuthFlow({ + baseUrl, + getOutput, + appName, +}) { + const email = `${appName}-${Date.now()}@app.test` + const password = 'secret-secret' + const nextPassword = 'secret-secret-2' + const clientIp = `127.0.0.${(Date.now() % 200) + 1}` + const requestHeaders = { + 'x-forwarded-for': clientIp, + 'x-real-ip': clientIp, + } + + const fetchAuthText = (path, options = {}) => fetchText(baseUrl, path, { + ...options, + headers: { + ...requestHeaders, + ...(options.headers ?? {}), + }, + }) + const fetchAuthJson = (path, options = {}) => fetchJson(baseUrl, path, { + ...options, + headers: { + ...requestHeaders, + ...(options.headers ?? {}), + }, + }) + + const loginPage = await fetchAuthText('/login') + assert.match(loginPage.text, /Sign in/i) + + const registerPage = await fetchAuthText('/register') + assert.match(registerPage.text, /Create account/i) + + const forgotPasswordPage = await fetchAuthText('/forgot-password') + assert.match(forgotPasswordPage.text, /Forgot password/i) + + const verifyPromptPage = await fetchAuthText('/verify-email') + assert.match(verifyPromptPage.text, /Verify your email/i) + + const initialUser = await fetchAuthJson('/api/auth/user') + assert.equal(initialUser.json.authenticated, false) + assert.equal(initialUser.json.user, null) + + const registerOutputStart = getOutput().length + const registeredJar = createCookieJar() + const registered = await fetchAuthJson('/api/register', { + fields: { + name: 'Auth Flow User', + email, + password, + passwordConfirmation: password, + }, + jar: registeredJar, + }) + assert.equal(registered.response.status, 201) + assert.equal(registered.json.ok, true) + assert.equal(registered.json.data?.message, 'Account created. Check your inbox to verify your email address.') + assert.equal(registered.json.data?.redirectTo, `/verify-email?email=${encodeURIComponent(email)}`) + assert.ok(listSetCookieHeaders(registered.response).length > 0) + + const verificationToken = ( + await waitForOutputMatch( + getOutput, + authTokenPattern, + registerOutputStart, + ) + )[1] + assert.ok(verificationToken) + + const pendingVerificationJar = createCookieJar() + const unverifiedLogin = await fetchAuthJson('/api/login', { + fields: { + email, + password, + }, + jar: pendingVerificationJar, + }) + assert.equal(unverifiedLogin.json.ok, true) + assert.equal(unverifiedLogin.json.data?.redirectTo, `/verify-email?email=${encodeURIComponent(email)}`) + + const resendOutputStart = getOutput().length + const resent = await fetchAuthJson('/api/verify-email/resend', { + method: 'POST', + body: JSON.stringify({ email }), + headers: { + 'content-type': 'application/json', + }, + }) + assert.equal(resent.json.ok, true) + assert.equal(resent.json.data?.message, 'A fresh verification email has been sent.') + + const resentVerificationToken = ( + await waitForOutputMatch( + getOutput, + authTokenPattern, + resendOutputStart, + ) + )[1] + assert.ok(resentVerificationToken) + + const verifyTokenPage = await fetchAuthText( + `/verify-email?token=${encodeURIComponent(resentVerificationToken)}`, + ) + assert.match(verifyTokenPage.text, /Verify your email/i) + + const verified = await fetchAuthJson('/api/verify-email', { + method: 'POST', + fields: { + token: resentVerificationToken, + }, + }) + assert.equal(verified.json.ok, true) + assert.equal(verified.json.data?.redirectTo, '/login') + + const authenticatedJar = createCookieJar() + const loggedIn = await fetchAuthJson('/api/login', { + fields: { + email, + password, + }, + jar: authenticatedJar, + }) + assert.equal(loggedIn.json.ok, true) + assert.equal(loggedIn.json.data?.message, 'Signed in successfully.') + assert.equal(loggedIn.json.data?.redirectTo, '/admin') + assert.ok(listSetCookieHeaders(loggedIn.response).length > 0) + + const authenticatedUser = await fetchAuthJson('/api/auth/user', { + jar: authenticatedJar, + }) + assert.equal(authenticatedUser.json.authenticated, true) + assert.equal(authenticatedUser.json.guard, 'web') + assert.equal(authenticatedUser.json.user?.email, email) + assert.equal(authenticatedUser.json.user?.name, 'Auth Flow User') + + const outputStart = getOutput().length + const forgotPassword = await fetchAuthJson('/api/forgot-password', { + fields: { + email, + }, + }) + assert.equal(forgotPassword.json.ok, true) + assert.equal(forgotPassword.json.data?.message, 'If an account exists for that email, a reset link has been sent.') + + const resetTokenMatch = await waitForOutputMatch( + getOutput, + authTokenPattern, + outputStart, + ) + const resetToken = resetTokenMatch[1] + assert.ok(resetToken) + + const resetPage = await fetchAuthText(`/reset-password?token=${encodeURIComponent(resetToken)}`) + assert.match(resetPage.text, /Reset password/i) + + const resetResult = await fetchAuthJson('/api/reset-password', { + fields: { + token: resetToken, + password: nextPassword, + passwordConfirmation: nextPassword, + }, + }) + assert.equal(resetResult.json.ok, true) + assert.equal(resetResult.json.data?.message, 'Password reset successfully. You can sign in with your new password.') + assert.equal(resetResult.json.data?.redirectTo, '/login') + + const oldPasswordLogin = await fetchAuthJson('/api/login', { + fields: { + email, + password, + }, + allowFailure: true, + }) + assert.equal(oldPasswordLogin.json.ok, false) + + const refreshedJar = createCookieJar() + const newPasswordLogin = await fetchAuthJson('/api/login', { + fields: { + email, + password: nextPassword, + }, + jar: refreshedJar, + }) + assert.equal(newPasswordLogin.json.ok, true) + assert.ok(listSetCookieHeaders(newPasswordLogin.response).length > 0) + + const refreshedUser = await fetchAuthJson('/api/auth/user', { + jar: refreshedJar, + }) + assert.equal(refreshedUser.json.authenticated, true) + assert.equal(refreshedUser.json.guard, 'web') + assert.equal(refreshedUser.json.user?.email, email) + assert.equal(refreshedUser.json.user?.name, 'Auth Flow User') +}