Use kv datastore for auth (#272)

This commit is contained in:
Jeremy 2024-03-19 04:37:30 +03:00 committed by GitHub
parent d741351e26
commit b6cab643ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 143 additions and 173 deletions

View file

@ -37,7 +37,7 @@ This template ships with OpenAI `gpt-3.5-turbo` as the default. However, thanks
You can deploy your own version of the Next.js AI Chatbot to Vercel with one click: You can deploy your own version of the Next.js AI Chatbot to Vercel with one click:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=Next.js+Chat&demo-description=A+full-featured%2C+hackable+Next.js+AI+chatbot+built+by+Vercel+Labs&demo-url=https%3A%2F%2Fchat.vercel.ai%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4aVPvWuTmBvzM5cEdRdqeW%2F4234f9baf160f68ffb385a43c3527645%2FCleanShot_2023-06-16_at_17.09.21.png&project-name=Next.js+Chat&repository-name=nextjs-chat&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot&from=templates&skippable-integrations=1&env=OPENAI_API_KEY%2CAUTH_SECRET&envDescription=How+to+get+these+env+vars&envLink=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&teamCreateStatus=hidden&stores=[{"type":"kv"},{"type":"postgres"}]) [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=Next.js+Chat&demo-description=A+full-featured%2C+hackable+Next.js+AI+chatbot+built+by+Vercel+Labs&demo-url=https%3A%2F%2Fchat.vercel.ai%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4aVPvWuTmBvzM5cEdRdqeW%2F4234f9baf160f68ffb385a43c3527645%2FCleanShot_2023-06-16_at_17.09.21.png&project-name=Next.js+Chat&repository-name=nextjs-chat&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot&from=templates&skippable-integrations=1&env=OPENAI_API_KEY%2CAUTH_SECRET&envDescription=How+to+get+these+env+vars&envLink=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&teamCreateStatus=hidden&stores=[{"type":"kv"}])
## Creating a KV Database Instance ## Creating a KV Database Instance
@ -57,7 +57,6 @@ You will need to use the environment variables [defined in `.env.example`](.env.
```bash ```bash
pnpm install pnpm install
pnpm seed
pnpm dev pnpm dev
``` ```

View file

@ -1,14 +1,26 @@
'use server' 'use server'
import { signIn } from '@/auth' import { signIn } from '@/auth'
import { AuthResult } from '@/lib/types' import { User } from '@/lib/types'
import { AuthError } from 'next-auth' import { AuthError } from 'next-auth'
import { z } from 'zod' import { z } from 'zod'
import { kv } from '@vercel/kv'
import { ResultCode } from '@/lib/utils'
export async function getUser(email: string) {
const user = await kv.hgetall<User>(`user:${email}`)
return user
}
interface Result {
type: string
resultCode: ResultCode
}
export async function authenticate( export async function authenticate(
_prevState: AuthResult | undefined, _prevState: Result | undefined,
formData: FormData formData: FormData
) { ): Promise<Result | undefined> {
try { try {
const email = formData.get('email') const email = formData.get('email')
const password = formData.get('password') const password = formData.get('password')
@ -27,24 +39,33 @@ export async function authenticate(
await signIn('credentials', { await signIn('credentials', {
email, email,
password, password,
redirectTo: '/' redirect: false
}) })
return {
type: 'success',
resultCode: ResultCode.UserLoggedIn
}
} else { } else {
return { type: 'error', message: 'Invalid credentials!' } return {
type: 'error',
resultCode: ResultCode.InvalidCredentials
}
} }
} catch (error) { } catch (error) {
if (error instanceof AuthError) { if (error instanceof AuthError) {
switch (error.type) { switch (error.type) {
case 'CredentialsSignin': case 'CredentialsSignin':
return { type: 'error', message: 'Invalid credentials!' } return {
type: 'error',
resultCode: ResultCode.InvalidCredentials
}
default: default:
return { return {
type: 'error', type: 'error',
message: 'Something went wrong, please try again!' resultCode: ResultCode.UnknownError
} }
} }
} }
throw error
} }
} }

View file

@ -1,15 +1,50 @@
'use server' 'use server'
import { signIn } from '@/auth' import { signIn } from '@/auth'
import { db } from '@vercel/postgres' import { ResultCode, getStringFromBuffer } from '@/lib/utils'
import { getStringFromBuffer } from '@/lib/utils'
import { z } from 'zod' import { z } from 'zod'
import { AuthResult } from '@/lib/types' import { kv } from '@vercel/kv'
import { getUser } from '../login/actions'
import { AuthError } from 'next-auth'
export async function createUser(
email: string,
hashedPassword: string,
salt: string
) {
const existingUser = await getUser(email)
if (existingUser) {
return {
type: 'error',
resultCode: ResultCode.UserAlreadyExists
}
} else {
const user = {
id: crypto.randomUUID(),
email,
password: hashedPassword,
salt
}
await kv.hmset(`user:${email}`, user)
return {
type: 'success',
resultCode: ResultCode.UserCreated
}
}
}
interface Result {
type: string
resultCode: ResultCode
}
export async function signup( export async function signup(
_prevState: AuthResult | undefined, _prevState: Result | undefined,
formData: FormData formData: FormData
) { ): Promise<Result | undefined> {
const email = formData.get('email') as string const email = formData.get('email') as string
const password = formData.get('password') as string const password = formData.get('password') as string
@ -34,42 +69,43 @@ export async function signup(
) )
const hashedPassword = getStringFromBuffer(hashedPasswordBuffer) const hashedPassword = getStringFromBuffer(hashedPasswordBuffer)
const client = await db.connect()
try { try {
await client.sql` const result = await createUser(email, hashedPassword, salt)
INSERT INTO users (email, password, salt)
VALUES (${email}, ${hashedPassword}, ${salt})
ON CONFLICT (id) DO NOTHING;
`
await signIn('credentials', { if (result.resultCode === ResultCode.UserCreated) {
email, await signIn('credentials', {
password, email,
redirect: false password,
}) redirect: false
})
}
return { type: 'success', message: 'Account created!' } return result
} catch (error) { } catch (error) {
const { message } = error as Error if (error instanceof AuthError) {
switch (error.type) {
if ( case 'CredentialsSignin':
message.startsWith('duplicate key value violates unique constraint') return {
) { type: 'error',
return { type: 'error', message: 'User already exists! Please log in.' } resultCode: ResultCode.InvalidCredentials
}
default:
return {
type: 'error',
resultCode: ResultCode.UnknownError
}
}
} else { } else {
return { return {
type: 'error', type: 'error',
message: 'Something went wrong! Please try again.' resultCode: ResultCode.UnknownError
} }
} }
} finally {
client.release()
} }
} else { } else {
return { return {
type: 'error', type: 'error',
message: 'Invalid entries, please try again!' resultCode: ResultCode.InvalidCredentials
} }
} }
} }

19
auth.ts
View file

@ -2,25 +2,8 @@ import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials' import Credentials from 'next-auth/providers/credentials'
import { authConfig } from './auth.config' import { authConfig } from './auth.config'
import { z } from 'zod' import { z } from 'zod'
import { sql } from '@vercel/postgres'
import { getStringFromBuffer } from './lib/utils' import { getStringFromBuffer } from './lib/utils'
import { getUser } from './app/login/actions'
interface User {
id: string
name: string
email: string
password: string
salt: string
}
async function getUser(email: string): Promise<User | undefined> {
try {
const user = await sql<User>`SELECT * FROM users WHERE email=${email}`
return user.rows[0]
} catch (error) {
throw new Error('Failed to fetch user.')
}
}
export const { auth, signIn, signOut } = NextAuth({ export const { auth, signIn, signOut } = NextAuth({
...authConfig, ...authConfig,

View file

@ -6,26 +6,30 @@ import Link from 'next/link'
import { useEffect } from 'react' import { useEffect } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { IconSpinner } from './ui/icons' import { IconSpinner } from './ui/icons'
import { getMessageFromCode } from '@/lib/utils'
import { useRouter } from 'next/navigation'
export default function LoginForm() { export default function LoginForm() {
const router = useRouter()
const [result, dispatch] = useFormState(authenticate, undefined) const [result, dispatch] = useFormState(authenticate, undefined)
useEffect(() => { useEffect(() => {
if (result) { if (result) {
if (result.type === 'error') { if (result.type === 'error') {
toast.error(result.message) toast.error(getMessageFromCode(result.resultCode))
} else { } else {
toast.success(result.message) toast.success(getMessageFromCode(result.resultCode))
router.refresh()
} }
} }
}, [result]) }, [result, router])
return ( return (
<form <form
action={dispatch} action={dispatch}
className="flex flex-col items-center gap-4 space-y-3" className="flex flex-col items-center gap-4 space-y-3"
> >
<div className="w-full flex-1 rounded-lg border bg-white px-6 pb-4 pt-8 shadow-md dark:bg-zinc-950 md:w-96"> <div className="w-full flex-1 rounded-lg border bg-white px-6 pb-4 pt-8 shadow-md md:w-96 dark:bg-zinc-950">
<h1 className="mb-3 text-2xl font-bold">Please log in to continue.</h1> <h1 className="mb-3 text-2xl font-bold">Please log in to continue.</h1>
<div className="w-full"> <div className="w-full">
<div> <div>
@ -84,7 +88,7 @@ function LoginButton() {
return ( return (
<button <button
className="flex flex-row justify-center items-center my-4 h-10 w-full rounded-md bg-zinc-900 p-2 text-sm font-semibold text-zinc-100 hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200" className="my-4 flex h-10 w-full flex-row items-center justify-center rounded-md bg-zinc-900 p-2 text-sm font-semibold text-zinc-100 hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
aria-disabled={pending} aria-disabled={pending}
> >
{pending ? <IconSpinner /> : 'Log in'} {pending ? <IconSpinner /> : 'Log in'}

View file

@ -6,6 +6,7 @@ import Link from 'next/link'
import { useEffect } from 'react' import { useEffect } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { IconSpinner } from './ui/icons' import { IconSpinner } from './ui/icons'
import { getMessageFromCode } from '@/lib/utils'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
export default function SignupForm() { export default function SignupForm() {
@ -15,10 +16,10 @@ export default function SignupForm() {
useEffect(() => { useEffect(() => {
if (result) { if (result) {
if (result.type === 'error') { if (result.type === 'error') {
toast.error(result.message) toast.error(getMessageFromCode(result.resultCode))
} else { } else {
toast.success(getMessageFromCode(result.resultCode))
router.refresh() router.refresh()
toast.success(result.message)
} }
} }
}, [result, router]) }, [result, router])
@ -28,7 +29,7 @@ export default function SignupForm() {
action={dispatch} action={dispatch}
className="flex flex-col items-center gap-4 space-y-3" className="flex flex-col items-center gap-4 space-y-3"
> >
<div className="w-full flex-1 rounded-lg border bg-white px-6 pb-4 pt-8 shadow-md dark:bg-zinc-950 md:w-96"> <div className="w-full flex-1 rounded-lg border bg-white px-6 pb-4 pt-8 shadow-md md:w-96 dark:bg-zinc-950">
<h1 className="mb-3 text-2xl font-bold">Sign up for an account!</h1> <h1 className="mb-3 text-2xl font-bold">Sign up for an account!</h1>
<div className="w-full"> <div className="w-full">
<div> <div>
@ -85,7 +86,7 @@ function LoginButton() {
return ( return (
<button <button
className="flex flex-row justify-center items-center my-4 h-10 w-full rounded-md bg-zinc-900 p-2 text-sm font-semibold text-zinc-100 hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200" className="my-4 flex h-10 w-full flex-row items-center justify-center rounded-md bg-zinc-900 p-2 text-sm font-semibold text-zinc-100 hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
aria-disabled={pending} aria-disabled={pending}
> >
{pending ? <IconSpinner /> : 'Create account'} {pending ? <IconSpinner /> : 'Create account'}

View file

@ -28,3 +28,10 @@ export interface AuthResult {
type: string type: string
message: string message: string
} }
export interface User extends Record<string, any> {
id: string
email: string
password: string
salt: string
}

View file

@ -61,3 +61,29 @@ export const getStringFromBuffer = (buffer: ArrayBuffer) =>
Array.from(new Uint8Array(buffer)) Array.from(new Uint8Array(buffer))
.map(b => b.toString(16).padStart(2, '0')) .map(b => b.toString(16).padStart(2, '0'))
.join('') .join('')
export enum ResultCode {
InvalidCredentials = 'INVALID_CREDENTIALS',
InvalidSubmission = 'INVALID_SUBMISSION',
UserAlreadyExists = 'USER_ALREADY_EXISTS',
UnknownError = 'UNKNOWN_ERROR',
UserCreated = 'USER_CREATED',
UserLoggedIn = 'USER_LOGGED_IN'
}
export const getMessageFromCode = (resultCode: string) => {
switch (resultCode) {
case ResultCode.InvalidCredentials:
return 'Invalid credentials!'
case ResultCode.InvalidSubmission:
return 'Invalid submission, please try again!'
case ResultCode.UserAlreadyExists:
return 'User already exists, please log in!'
case ResultCode.UserCreated:
return 'User created, welcome!'
case ResultCode.UnknownError:
return 'Something went wrong, please try again!'
case ResultCode.UserLoggedIn:
return 'Logged in!'
}
}

View file

@ -26,7 +26,6 @@
"@vercel/analytics": "^1.1.2", "@vercel/analytics": "^1.1.2",
"@vercel/kv": "^1.0.1", "@vercel/kv": "^1.0.1",
"@vercel/og": "^0.6.2", "@vercel/og": "^0.6.2",
"@vercel/postgres": "^0.7.2",
"ai": "^3.0.12", "ai": "^3.0.12",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",

106
pnpm-lock.yaml generated
View file

@ -44,9 +44,6 @@ dependencies:
'@vercel/og': '@vercel/og':
specifier: ^0.6.2 specifier: ^0.6.2
version: 0.6.2 version: 0.6.2
'@vercel/postgres':
specifier: ^0.7.2
version: 0.7.2
ai: ai:
specifier: ^3.0.12 specifier: ^3.0.12
version: 3.0.12(react@18.2.0)(solid-js@1.8.11)(svelte@4.2.9)(vue@3.4.15)(zod@3.22.4) version: 3.0.12(react@18.2.0)(solid-js@1.8.11)(svelte@4.2.9)(vue@3.4.15)(zod@3.22.4)
@ -384,12 +381,6 @@ packages:
'@jridgewell/resolve-uri': 3.1.1 '@jridgewell/resolve-uri': 3.1.1
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
/@neondatabase/serverless@0.7.2:
resolution: {integrity: sha512-wU3WA2uTyNO7wjPs3Mg0G01jztAxUxzd9/mskMmtPwPTjf7JKWi9AW5/puOGXLxmZ9PVgRFeBVRVYq5nBPhsCg==}
dependencies:
'@types/pg': 8.6.6
dev: false
/@next/env@14.1.3: /@next/env@14.1.3:
resolution: {integrity: sha512-VhgXTvrgeBRxNPjyfBsDIMvgsKDxjlpw4IAUsHCX8Gjl1vtHUYRT3+xfQ/wwvLPDd/6kqfLqk9Pt4+7gysuCKQ==} resolution: {integrity: sha512-VhgXTvrgeBRxNPjyfBsDIMvgsKDxjlpw4IAUsHCX8Gjl1vtHUYRT3+xfQ/wwvLPDd/6kqfLqk9Pt4+7gysuCKQ==}
dev: false dev: false
@ -1333,14 +1324,6 @@ packages:
dependencies: dependencies:
undici-types: 5.26.5 undici-types: 5.26.5
/@types/pg@8.6.6:
resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==}
dependencies:
'@types/node': 20.11.5
pg-protocol: 1.6.0
pg-types: 2.2.0
dev: false
/@types/prop-types@15.7.11: /@types/prop-types@15.7.11:
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
@ -1465,16 +1448,6 @@ packages:
yoga-wasm-web: 0.3.3 yoga-wasm-web: 0.3.3
dev: false dev: false
/@vercel/postgres@0.7.2:
resolution: {integrity: sha512-IqR/ZAvoPGcPaXl9eWWB5KaA+w/81RzZa/18P4izQRHpNBkTGt9HwGfYi9+wut5UgxNq4QSX9A7HIQR6QDvX2Q==}
engines: {node: '>=14.6'}
dependencies:
'@neondatabase/serverless': 0.7.2
bufferutil: 4.0.8
utf-8-validate: 6.0.3
ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3)
dev: false
/@vue/compiler-core@3.4.15: /@vue/compiler-core@3.4.15:
resolution: {integrity: sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==} resolution: {integrity: sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==}
dependencies: dependencies:
@ -1857,14 +1830,6 @@ packages:
update-browserslist-db: 1.0.13(browserslist@4.22.2) update-browserslist-db: 1.0.13(browserslist@4.22.2)
dev: true dev: true
/bufferutil@4.0.8:
resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
engines: {node: '>=6.14.2'}
requiresBuild: true
dependencies:
node-gyp-build: 4.8.0
dev: false
/busboy@1.6.0: /busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'} engines: {node: '>=10.16.0'}
@ -4125,11 +4090,6 @@ packages:
whatwg-url: 5.0.0 whatwg-url: 5.0.0
dev: false dev: false
/node-gyp-build@4.8.0:
resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==}
hasBin: true
dev: false
/node-releases@2.0.14: /node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
dev: true dev: true
@ -4337,26 +4297,6 @@ packages:
is-reference: 3.0.2 is-reference: 3.0.2
dev: false dev: false
/pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
dev: false
/pg-protocol@1.6.0:
resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==}
dev: false
/pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
dependencies:
pg-int8: 1.0.1
postgres-array: 2.0.0
postgres-bytea: 1.0.0
postgres-date: 1.0.7
postgres-interval: 1.2.0
dev: false
/picocolors@1.0.0: /picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
@ -4460,28 +4400,6 @@ packages:
picocolors: 1.0.0 picocolors: 1.0.0
source-map-js: 1.0.2 source-map-js: 1.0.2
/postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
dev: false
/postgres-bytea@1.0.0:
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
engines: {node: '>=0.10.0'}
dev: false
/postgres-date@1.0.7:
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
engines: {node: '>=0.10.0'}
dev: false
/postgres-interval@1.2.0:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
dependencies:
xtend: 4.0.2
dev: false
/preact-render-to-string@5.2.3(preact@10.11.3): /preact-render-to-string@5.2.3(preact@10.11.3):
resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==} resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==}
peerDependencies: peerDependencies:
@ -5528,14 +5446,6 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/utf-8-validate@6.0.3:
resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==}
engines: {node: '>=6.14.2'}
requiresBuild: true
dependencies:
node-gyp-build: 4.8.0
dev: false
/util-deprecate@1.0.2: /util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true dev: true
@ -5682,22 +5592,6 @@ packages:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true dev: true
/ws@8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3):
resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dependencies:
bufferutil: 4.0.8
utf-8-validate: 6.0.3
dev: false
/xtend@4.0.2: /xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}