diff --git a/app/afrolife/[id]/page.tsx b/app/afrolife/[id]/page.tsx index 54ec013..41d2cb7 100644 --- a/app/afrolife/[id]/page.tsx +++ b/app/afrolife/[id]/page.tsx @@ -2,6 +2,7 @@ import React from 'react'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import { ArrowLeft, Share2, Play, Calendar, User, Building2, MapPin, ArrowRight } from 'lucide-react'; +import { sanitizeHTML } from '../../../lib/sanitize'; import { prisma } from '../../../lib/prisma'; import { InterviewType } from '@prisma/client'; @@ -158,10 +159,10 @@ export default async function AfroLifeDetailPage({ params }: Props) { {/* Description / Article Content / Event Content */}
{isEvent ? ( -
+
) : ( !isVideo && (interview as any).content ? ( -
+
) : (

{(interview as any).excerpt} diff --git a/app/afrolife/page.tsx b/app/afrolife/page.tsx index 740103e..90d563c 100644 --- a/app/afrolife/page.tsx +++ b/app/afrolife/page.tsx @@ -1,6 +1,7 @@ import React from 'react'; import Link from 'next/link'; import { Play, FileText, Clock, Mic, Calendar } from 'lucide-react'; +import { sanitizeHTML } from '../../lib/sanitize'; import { prisma } from '../../lib/prisma'; import { InterviewType } from '@prisma/client'; import EventTimeline from '../../components/EventTimeline'; @@ -169,7 +170,7 @@ export default async function AfroLifePage({ searchParams }: Props) {

{item.title}

-
+
); diff --git a/app/api/auth/csrf/route.ts b/app/api/auth/csrf/route.ts new file mode 100644 index 0000000..0768339 --- /dev/null +++ b/app/api/auth/csrf/route.ts @@ -0,0 +1,7 @@ +import { NextResponse } from 'next/server'; +import { generateCsrfToken } from '@/lib/csrf'; + +export async function GET() { + const token = await generateCsrfToken(); + return NextResponse.json({ csrfToken: token }); +} diff --git a/app/api/auth/forgot-password/route.ts b/app/api/auth/forgot-password/route.ts index 8a72419..ae7f234 100644 --- a/app/api/auth/forgot-password/route.ts +++ b/app/api/auth/forgot-password/route.ts @@ -1,9 +1,22 @@ -import { NextResponse } from 'next/server'; +import { NextResponse, NextRequest } from 'next/server'; import prisma from '@/lib/prisma'; import { sendEmail } from '@/lib/mail'; import crypto from 'crypto'; -export async function POST(req: Request) { +import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf'; + +export async function POST(req: NextRequest) { + // CSRF Protection + const isOriginValid = verifyOrigin(req); + const isCsrfValid = await verifyCsrfToken(req); + + if (!isOriginValid || !isCsrfValid) { + return NextResponse.json( + { error: 'Protection CSRF : Requête invalide' }, + { status: 403 } + ); + } + try { const { email } = await req.json(); diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 8f95138..1553a29 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -2,7 +2,20 @@ import { NextRequest, NextResponse } from 'next/server'; import prisma from '@/lib/prisma'; import bcrypt from 'bcryptjs'; +import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf'; + export async function POST(request: NextRequest) { + // CSRF Protection + const isOriginValid = verifyOrigin(request); + const isCsrfValid = await verifyCsrfToken(request); + + if (!isOriginValid || !isCsrfValid) { + return NextResponse.json( + { error: 'Protection CSRF : Requête invalide' }, + { status: 403 } + ); + } + try { const { email, password } = await request.json(); diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 6f931b4..4bb839f 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -3,7 +3,20 @@ import prisma from '@/lib/prisma'; import bcrypt from 'bcryptjs'; import crypto from 'crypto'; +import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf'; + export async function POST(request: NextRequest) { + // CSRF Protection + const isOriginValid = verifyOrigin(request); + const isCsrfValid = await verifyCsrfToken(request); + + if (!isOriginValid || !isCsrfValid) { + return NextResponse.json( + { error: 'Protection CSRF : Requête invalide' }, + { status: 403 } + ); + } + try { const { name, email, password } = await request.json(); diff --git a/app/api/auth/resend-verification/route.ts b/app/api/auth/resend-verification/route.ts index 94521ee..662d483 100644 --- a/app/api/auth/resend-verification/route.ts +++ b/app/api/auth/resend-verification/route.ts @@ -2,7 +2,20 @@ import { NextRequest, NextResponse } from 'next/server'; import prisma from '@/lib/prisma'; import crypto from 'crypto'; +import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf'; + export async function POST(request: NextRequest) { + // CSRF Protection + const isOriginValid = verifyOrigin(request); + const isCsrfValid = await verifyCsrfToken(request); + + if (!isOriginValid || !isCsrfValid) { + return NextResponse.json( + { error: 'Protection CSRF : Requête invalide' }, + { status: 403 } + ); + } + try { const { email } = await request.json(); diff --git a/app/api/auth/reset-password/route.ts b/app/api/auth/reset-password/route.ts index 23246a8..36b1366 100644 --- a/app/api/auth/reset-password/route.ts +++ b/app/api/auth/reset-password/route.ts @@ -1,10 +1,22 @@ -import { NextResponse } from 'next/server'; +import { NextResponse, NextRequest } from 'next/server'; import prisma from '@/lib/prisma'; import bcrypt from 'bcryptjs'; +import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf'; + +export async function POST(request: NextRequest) { + // CSRF Protection + const isOriginValid = verifyOrigin(request); + const isCsrfValid = await verifyCsrfToken(request); + + if (!isOriginValid || !isCsrfValid) { + return NextResponse.json( + { error: 'Protection CSRF : Requête invalide' }, + { status: 403 } + ); + } -export async function POST(req: Request) { try { - const { token, password } = await req.json(); + const { token, password } = await request.json(); if (!token || !password) { return NextResponse.json({ error: 'Token et mot de passe sont requis' }, { status: 400 }); diff --git a/app/blog/[id]/page.tsx b/app/blog/[id]/page.tsx index 077c5a8..417be86 100644 --- a/app/blog/[id]/page.tsx +++ b/app/blog/[id]/page.tsx @@ -2,6 +2,7 @@ import React from 'react'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import { ArrowLeft, User, Calendar, Share2 } from 'lucide-react'; +import { sanitizeHTML } from '../../../lib/sanitize'; import { prisma } from '../../../lib/prisma'; import { Metadata } from 'next'; @@ -107,7 +108,7 @@ export default async function BlogPostPage({ params }: Props) {

diff --git a/app/cgu/page.tsx b/app/cgu/page.tsx index 83e16ed..062ce59 100644 --- a/app/cgu/page.tsx +++ b/app/cgu/page.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { getLegalDocument } from '@/lib/legal'; +import { sanitizeHTML } from '@/lib/sanitize'; import { getSiteSettings } from '@/lib/settings'; import { Metadata } from 'next'; @@ -32,7 +33,7 @@ export default async function CGUPage() {
diff --git a/app/cgv/page.tsx b/app/cgv/page.tsx index 29a59ff..64a3028 100644 --- a/app/cgv/page.tsx +++ b/app/cgv/page.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { getLegalDocument } from '@/lib/legal'; +import { sanitizeHTML } from '@/lib/sanitize'; import { getSiteSettings } from '@/lib/settings'; import { Metadata } from 'next'; @@ -32,7 +33,7 @@ export default async function CGVPage() {
diff --git a/app/forgot-password/page.tsx b/app/forgot-password/page.tsx index 2449014..4ad29a2 100644 --- a/app/forgot-password/page.tsx +++ b/app/forgot-password/page.tsx @@ -7,10 +7,19 @@ import { useUser } from '@/components/UserProvider'; const ForgotPasswordPage = () => { const { settings } = useUser(); const siteName = settings?.siteName || "Afrohub"; + + // Fetch CSRF token on mount + React.useEffect(() => { + fetch('/api/auth/csrf') + .then(res => res.json()) + .then(data => setCsrfToken(data.csrfToken)) + .catch(err => console.error('Error fetching CSRF token:', err)); + }, []); const [email, setEmail] = useState(''); const [message, setMessage] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + const [csrfToken, setCsrfToken] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -21,7 +30,10 @@ const ForgotPasswordPage = () => { try { const res = await fetch('/api/auth/forgot-password', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'x-csrf-token': csrfToken + }, body: JSON.stringify({ email }), }); @@ -61,6 +73,7 @@ const ForgotPasswordPage = () => {
) : (
+ {error && (
{error} diff --git a/app/login/page.tsx b/app/login/page.tsx index d5f04f2..a8ac34e 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -17,11 +17,20 @@ const LoginPage = () => { const [loading, setLoading] = useState(false); const [resendLoading, setResendLoading] = useState(false); const [resendMessage, setResendMessage] = useState(''); + const [csrfToken, setCsrfToken] = useState(''); const router = useRouter(); const searchParams = useSearchParams(); const verified = searchParams.get('verified') === 'true'; const queryError = searchParams.get('error'); + + // Fetch CSRF token on mount + React.useEffect(() => { + fetch('/api/auth/csrf') + .then(res => res.json()) + .then(data => setCsrfToken(data.csrfToken)) + .catch(err => console.error('Error fetching CSRF token:', err)); + }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -33,7 +42,10 @@ const LoginPage = () => { try { const res = await fetch('/api/auth/login', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'x-csrf-token': csrfToken + }, body: JSON.stringify({ email, password }), }); @@ -64,7 +76,10 @@ const LoginPage = () => { try { const res = await fetch('/api/auth/resend-verification', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'x-csrf-token': csrfToken + }, body: JSON.stringify({ email }), }); @@ -131,6 +146,7 @@ const LoginPage = () => { )} +
{ const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); + const [csrfToken, setCsrfToken] = useState(''); const handleChange = (e: React.ChangeEvent) => { setFormData({ ...formData, [e.target.name]: e.target.value }); setError(''); }; + + // Fetch CSRF token on mount + React.useEffect(() => { + fetch('/api/auth/csrf') + .then(res => res.json()) + .then(data => setCsrfToken(data.csrfToken)) + .catch(err => console.error('Error fetching CSRF token:', err)); + }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -39,7 +48,10 @@ const RegisterPage = () => { try { const res = await fetch('/api/auth/register', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'x-csrf-token': csrfToken + }, body: JSON.stringify({ name: formData.name, email: formData.email, @@ -87,6 +99,7 @@ const RegisterPage = () => {
) : ( + {error && (
diff --git a/app/reset-password/page.tsx b/app/reset-password/page.tsx index fcec500..40ef7c6 100644 --- a/app/reset-password/page.tsx +++ b/app/reset-password/page.tsx @@ -13,9 +13,18 @@ const ResetPasswordForm = () => { const [message, setMessage] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + const [csrfToken, setCsrfToken] = useState(''); const router = useRouter(); const searchParams = useSearchParams(); const token = searchParams.get('token'); + + // Fetch CSRF token on mount + React.useEffect(() => { + fetch('/api/auth/csrf') + .then(res => res.json()) + .then(data => setCsrfToken(data.csrfToken)) + .catch(err => console.error('Error fetching CSRF token:', err)); + }, []); useEffect(() => { if (!token) { @@ -43,7 +52,10 @@ const ResetPasswordForm = () => { try { const res = await fetch('/api/auth/reset-password', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'x-csrf-token': csrfToken + }, body: JSON.stringify({ token, password }), }); @@ -84,6 +96,7 @@ const ResetPasswordForm = () => {
) : ( + {error && (
{error} diff --git a/components/EventTimeline.tsx b/components/EventTimeline.tsx index ab2d059..31f5a53 100644 --- a/components/EventTimeline.tsx +++ b/components/EventTimeline.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Calendar, MapPin, ArrowRight, Clock } from 'lucide-react'; import Link from 'next/link'; +import { sanitizeHTML } from '../lib/sanitize'; interface Event { id: string; @@ -74,7 +75,7 @@ const EventTimeline = ({ events }: EventTimelineProps) => {
diff --git a/lib/csrf.ts b/lib/csrf.ts new file mode 100644 index 0000000..94b6035 --- /dev/null +++ b/lib/csrf.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import crypto from 'crypto'; + +const CSRF_COOKIE_NAME = 'csrf_token'; +const CSRF_HEADER_NAME = 'x-csrf-token'; + +export async function generateCsrfToken() { + const token = crypto.randomBytes(32).toString('hex'); + const cookieStore = await cookies(); + + cookieStore.set(CSRF_COOKIE_NAME, token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + }); + + return token; +} + +export async function verifyCsrfToken(request: NextRequest) { + const cookieStore = await cookies(); + const cookieToken = cookieStore.get(CSRF_COOKIE_NAME)?.value; + const headerToken = request.headers.get(CSRF_HEADER_NAME); + + if (!cookieToken || !headerToken || cookieToken !== headerToken) { + return false; + } + + return true; +} + +/** + * Basic Origin check as an additional layer of defense + */ +export function verifyOrigin(request: NextRequest) { + const origin = request.headers.get('origin'); + const host = request.headers.get('host'); + + if (!origin || !host) return false; + + // Remove protocol from origin for comparison if host doesn't have it + const originHost = origin.replace(/^https?:\/\//, ''); + + return originHost === host || originHost.startsWith(host + ':'); +} diff --git a/lib/sanitize.ts b/lib/sanitize.ts new file mode 100644 index 0000000..f508a01 --- /dev/null +++ b/lib/sanitize.ts @@ -0,0 +1,17 @@ +import DOMPurify from 'isomorphic-dompurify'; + +/** + * Sanitizes HTML content to prevent XSS attacks. + * Use this before passing content to dangerouslySetInnerHTML. + */ +export function sanitizeHTML(html: string): string { + return DOMPurify.sanitize(html, { + USE_PROFILES: { html: true }, + ALLOWED_TAGS: [ + 'p', 'br', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'span', 'div', + 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td' + ], + ALLOWED_ATTR: ['href', 'target', 'rel', 'src', 'alt', 'class', 'style'] + }); +} diff --git a/next.config.mjs b/next.config.mjs index 1d0e1ff..aa45b39 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -23,7 +23,7 @@ const nextConfig = { }, { key: 'Content-Security-Policy', - value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' https:; connect-src 'self' wss: https:; base-uri 'self'; form-action 'self'; frame-ancestors 'self';", + value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' https:; connect-src 'self' wss: https:; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; object-src 'none';", }, { key: 'X-Content-Type-Options', @@ -32,6 +32,18 @@ const nextConfig = { { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin', + }, + { + key: 'Strict-Transport-Security', + value: 'max-age=63072000; includeSubDomains; preload', + }, + { + key: 'X-DNS-Prefetch-Control', + value: 'on', + }, + { + key: 'X-XSS-Protection', + value: '1; mode=block', } ], }, diff --git a/package-lock.json b/package-lock.json index 5003f60..56ec50d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,13 @@ "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.19", "@types/bcryptjs": "^2.4.6", + "@types/dompurify": "^3.0.5", "@types/node": "^22.14.0", "@types/pg": "^8.16.0", "autoprefixer": "^10.4.27", "bcryptjs": "^3.0.3", "dotenv": "^17.3.1", + "isomorphic-dompurify": "^3.11.0", "jiti": "^2.6.1", "lucide-react": "^0.554.0", "next": "^16.1.6", @@ -55,6 +57,199 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "license": "MIT" + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", @@ -481,6 +676,23 @@ "node": ">=18" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@google/genai": { "version": "1.50.1", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.50.1.tgz", @@ -1706,6 +1918,15 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/node": { "version": "22.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", @@ -1753,6 +1974,12 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -1871,6 +2098,15 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -2123,6 +2359,19 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2271,6 +2520,19 @@ "node": ">= 12" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2288,6 +2550,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -2324,6 +2592,15 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz", + "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -2390,6 +2667,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-toolkit": { "version": "1.46.0", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.0.tgz", @@ -2670,6 +2959,18 @@ "node": ">=8" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -2712,6 +3013,25 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, + "node_modules/isomorphic-dompurify": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-3.11.0.tgz", + "integrity": "sha512-il0sNhLnfawc6vKoMqnZX1JXzEzw0pP3DZWg7mM7VJNJ8cq6DCxeAjEcfjTazyBF8dkUn+rBsBPS5NQs4ZaI3g==", + "license": "MIT", + "dependencies": { + "dompurify": "^3.4.1", + "jsdom": "^29.1.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -2721,6 +3041,46 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -3006,6 +3366,15 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/lucide-react": { "version": "0.554.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.554.0.tgz", @@ -3024,6 +3393,12 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3202,6 +3577,18 @@ "node": ">=8" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3464,6 +3851,15 @@ "node": ">=12.0.0" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -3626,6 +4022,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -3680,6 +4085,18 @@ ], "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3842,6 +4259,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", @@ -3876,6 +4299,48 @@ "node": ">=18" } }, + "node_modules/tldts": { + "version": "7.0.29", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.29.tgz", + "integrity": "sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.29" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.29", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.29.tgz", + "integrity": "sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -3924,6 +4389,15 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -3997,6 +4471,18 @@ "d3-timer": "^3.0.1" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -4006,6 +4492,38 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -4045,6 +4563,21 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 7786c34..4891224 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,13 @@ "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.19", "@types/bcryptjs": "^2.4.6", + "@types/dompurify": "^3.0.5", "@types/node": "^22.14.0", "@types/pg": "^8.16.0", "autoprefixer": "^10.4.27", "bcryptjs": "^3.0.3", "dotenv": "^17.3.1", + "isomorphic-dompurify": "^3.11.0", "jiti": "^2.6.1", "lucide-react": "^0.554.0", "next": "^16.1.6", @@ -59,4 +61,4 @@ "postcss": "^8.5.10", "@hono/node-server": "^1.19.13" } -} \ No newline at end of file +}