From 738e056dee1f02b0cdb1b43a78df2fc4c54b0b12 Mon Sep 17 00:00:00 2001 From: GURSEWAK13 Date: Thu, 26 Mar 2026 13:09:04 +0530 Subject: [PATCH 1/8] add documentation website and docs badge Next.js docs site with Fumadocs, Tailwind CSS. Landing page with hero, feature cards, service grid, comparison table, and provider code examples. Full docs covering all 16 services, cross-cutting features, configuration, error handling, and architecture. Blog section and full-text search. Deployed at https://cloudemu.vercel.app --- README.md | 1 + frontend/.gitignore | 7 + frontend/app/(home)/layout.tsx | 27 + frontend/app/(home)/page.tsx | 39 + frontend/app/api/search/route.ts | 4 + frontend/app/blog/[slug]/page.tsx | 71 + frontend/app/blog/layout.tsx | 27 + frontend/app/blog/page.tsx | 36 + frontend/app/docs/[[...slug]]/page.tsx | 48 + frontend/app/docs/layout.tsx | 30 + frontend/app/global.css | 9 + frontend/app/layout.tsx | 41 + frontend/components/landing/code-example.tsx | 110 + .../components/landing/comparison-table.tsx | 117 + frontend/components/landing/cta-section.tsx | 60 + frontend/components/landing/feature-cards.tsx | 74 + frontend/components/landing/hero.tsx | 98 + frontend/components/landing/service-grid.tsx | 55 + frontend/components/search-dialog.tsx | 62 + frontend/content/blog/hello-world.mdx | 50 + frontend/content/docs/architecture.mdx | 91 + frontend/content/docs/configuration.mdx | 109 + frontend/content/docs/error-handling.mdx | 91 + .../content/docs/features/error-injection.mdx | 71 + frontend/content/docs/features/fake-clock.mdx | 86 + frontend/content/docs/features/index.mdx | 29 + .../docs/features/latency-simulation.mdx | 50 + frontend/content/docs/features/meta.json | 12 + frontend/content/docs/features/metrics.mdx | 54 + .../content/docs/features/rate-limiting.mdx | 39 + frontend/content/docs/features/recording.mdx | 65 + frontend/content/docs/index.mdx | 59 + frontend/content/docs/installation.mdx | 81 + frontend/content/docs/meta.json | 18 + frontend/content/docs/portable-api.mdx | 92 + frontend/content/docs/prerequisites.mdx | 69 + frontend/content/docs/quick-start.mdx | 151 + frontend/content/docs/services/cache.mdx | 44 + frontend/content/docs/services/compute.mdx | 104 + .../docs/services/containerregistry.mdx | 51 + frontend/content/docs/services/database.mdx | 116 + frontend/content/docs/services/dns.mdx | 48 + frontend/content/docs/services/eventbus.mdx | 56 + frontend/content/docs/services/iam.mdx | 71 + frontend/content/docs/services/index.mdx | 55 + .../content/docs/services/loadbalancer.mdx | 57 + frontend/content/docs/services/logging.mdx | 49 + .../content/docs/services/messagequeue.mdx | 74 + frontend/content/docs/services/meta.json | 22 + frontend/content/docs/services/monitoring.mdx | 90 + frontend/content/docs/services/networking.mdx | 91 + .../content/docs/services/notification.mdx | 44 + frontend/content/docs/services/secrets.mdx | 53 + frontend/content/docs/services/serverless.mdx | 75 + frontend/content/docs/services/storage.mdx | 108 + frontend/lib/services.ts | 28 + frontend/lib/source.ts | 8 + frontend/next.config.mjs | 10 + frontend/package-lock.json | 6248 +++++++++++++++++ frontend/package.json | 32 + frontend/postcss.config.mjs | 8 + frontend/source.config.ts | 7 + frontend/tsconfig.json | 43 + 63 files changed, 9725 insertions(+) create mode 100644 frontend/.gitignore create mode 100644 frontend/app/(home)/layout.tsx create mode 100644 frontend/app/(home)/page.tsx create mode 100644 frontend/app/api/search/route.ts create mode 100644 frontend/app/blog/[slug]/page.tsx create mode 100644 frontend/app/blog/layout.tsx create mode 100644 frontend/app/blog/page.tsx create mode 100644 frontend/app/docs/[[...slug]]/page.tsx create mode 100644 frontend/app/docs/layout.tsx create mode 100644 frontend/app/global.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/components/landing/code-example.tsx create mode 100644 frontend/components/landing/comparison-table.tsx create mode 100644 frontend/components/landing/cta-section.tsx create mode 100644 frontend/components/landing/feature-cards.tsx create mode 100644 frontend/components/landing/hero.tsx create mode 100644 frontend/components/landing/service-grid.tsx create mode 100644 frontend/components/search-dialog.tsx create mode 100644 frontend/content/blog/hello-world.mdx create mode 100644 frontend/content/docs/architecture.mdx create mode 100644 frontend/content/docs/configuration.mdx create mode 100644 frontend/content/docs/error-handling.mdx create mode 100644 frontend/content/docs/features/error-injection.mdx create mode 100644 frontend/content/docs/features/fake-clock.mdx create mode 100644 frontend/content/docs/features/index.mdx create mode 100644 frontend/content/docs/features/latency-simulation.mdx create mode 100644 frontend/content/docs/features/meta.json create mode 100644 frontend/content/docs/features/metrics.mdx create mode 100644 frontend/content/docs/features/rate-limiting.mdx create mode 100644 frontend/content/docs/features/recording.mdx create mode 100644 frontend/content/docs/index.mdx create mode 100644 frontend/content/docs/installation.mdx create mode 100644 frontend/content/docs/meta.json create mode 100644 frontend/content/docs/portable-api.mdx create mode 100644 frontend/content/docs/prerequisites.mdx create mode 100644 frontend/content/docs/quick-start.mdx create mode 100644 frontend/content/docs/services/cache.mdx create mode 100644 frontend/content/docs/services/compute.mdx create mode 100644 frontend/content/docs/services/containerregistry.mdx create mode 100644 frontend/content/docs/services/database.mdx create mode 100644 frontend/content/docs/services/dns.mdx create mode 100644 frontend/content/docs/services/eventbus.mdx create mode 100644 frontend/content/docs/services/iam.mdx create mode 100644 frontend/content/docs/services/index.mdx create mode 100644 frontend/content/docs/services/loadbalancer.mdx create mode 100644 frontend/content/docs/services/logging.mdx create mode 100644 frontend/content/docs/services/messagequeue.mdx create mode 100644 frontend/content/docs/services/meta.json create mode 100644 frontend/content/docs/services/monitoring.mdx create mode 100644 frontend/content/docs/services/networking.mdx create mode 100644 frontend/content/docs/services/notification.mdx create mode 100644 frontend/content/docs/services/secrets.mdx create mode 100644 frontend/content/docs/services/serverless.mdx create mode 100644 frontend/content/docs/services/storage.mdx create mode 100644 frontend/lib/services.ts create mode 100644 frontend/lib/source.ts create mode 100644 frontend/next.config.mjs create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/source.config.ts create mode 100644 frontend/tsconfig.json diff --git a/README.md b/README.md index 5878805d..b3f6779c 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Go Version Providers Zero Cost + Documentation

--- diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..5b11c9e5 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.next/ +.source/ +out/ +.vercel/ +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/app/(home)/layout.tsx b/frontend/app/(home)/layout.tsx new file mode 100644 index 00000000..86e56aff --- /dev/null +++ b/frontend/app/(home)/layout.tsx @@ -0,0 +1,27 @@ +import { HomeLayout } from 'fumadocs-ui/layouts/home'; +import type { ReactNode } from 'react'; + +export default function Layout({ children }: { children: ReactNode }) { + return ( + + cloudemu + + ), + url: '/', + }} + links={[ + { text: 'Docs', url: '/docs' }, + { text: 'Blog', url: '/blog' }, + { + text: 'GitHub', + url: 'https://github.com/stackshy/cloudemu', + }, + ]} + > + {children} + + ); +} diff --git a/frontend/app/(home)/page.tsx b/frontend/app/(home)/page.tsx new file mode 100644 index 00000000..b88e084b --- /dev/null +++ b/frontend/app/(home)/page.tsx @@ -0,0 +1,39 @@ +import Link from 'next/link'; +import { Hero } from '@/components/landing/hero'; +import { ComparisonTable } from '@/components/landing/comparison-table'; +import { FeatureCards } from '@/components/landing/feature-cards'; +import { ServiceGrid } from '@/components/landing/service-grid'; +import { CodeExample } from '@/components/landing/code-example'; +import { CTASection } from '@/components/landing/cta-section'; + +export default function HomePage() { + return ( +
+ + + {/* Provider Logos */} +
+
+
+ AWS +
+
+ Azure +
+
+ GCP +
+
+

+ 48 services across 3 cloud providers — all in memory +

+
+ + + + + + +
+ ); +} diff --git a/frontend/app/api/search/route.ts b/frontend/app/api/search/route.ts new file mode 100644 index 00000000..df889626 --- /dev/null +++ b/frontend/app/api/search/route.ts @@ -0,0 +1,4 @@ +import { source } from '@/lib/source'; +import { createFromSource } from 'fumadocs-core/search/server'; + +export const { GET } = createFromSource(source); diff --git a/frontend/app/blog/[slug]/page.tsx b/frontend/app/blog/[slug]/page.tsx new file mode 100644 index 00000000..e40a2440 --- /dev/null +++ b/frontend/app/blog/[slug]/page.tsx @@ -0,0 +1,71 @@ +import { notFound } from 'next/navigation'; +import fs from 'fs'; +import path from 'path'; +import type { Metadata } from 'next'; + +const blogDir = path.join(process.cwd(), 'content/blog'); + +async function getBlogPost(slug: string) { + const filePath = path.join(blogDir, `${slug}.mdx`); + if (!fs.existsSync(filePath)) return null; + + const source = fs.readFileSync(filePath, 'utf-8'); + const frontmatterMatch = source.match(/^---\n([\s\S]*?)\n---/); + const content = source.replace(/^---\n[\s\S]*?\n---\n/, ''); + + let title = slug; + let description = ''; + if (frontmatterMatch) { + const fm = frontmatterMatch[1]; + const titleMatch = fm.match(/title:\s*(.*)/); + const descMatch = fm.match(/description:\s*(.*)/); + if (titleMatch) title = titleMatch[1].trim(); + if (descMatch) description = descMatch[1].trim(); + } + + return { title, description, content }; +} + +export default async function BlogPostPage(props: { + params: Promise<{ slug: string }>; +}) { + const params = await props.params; + const post = await getBlogPost(params.slug); + if (!post) notFound(); + + // Simple markdown-like rendering for blog posts + const lines = post.content.split('\n'); + const html = lines + .map((line) => { + if (line.startsWith('# ')) return `

${line.slice(2)}

`; + if (line.startsWith('## ')) return `

${line.slice(3)}

`; + if (line.startsWith('### ')) return `

${line.slice(4)}

`; + if (line.startsWith('- ')) return `
  • ${line.slice(2)}
  • `; + if (line.startsWith('```')) return ''; + if (line.trim() === '') return '
    '; + return `

    ${line}

    `; + }) + .join('\n'); + + return ( +
    +
    +

    {post.title}

    +

    {post.description}

    +
    +
    +
    + ); +} + +export async function generateMetadata(props: { + params: Promise<{ slug: string }>; +}): Promise { + const params = await props.params; + const post = await getBlogPost(params.slug); + if (!post) return {}; + return { title: post.title, description: post.description }; +} diff --git a/frontend/app/blog/layout.tsx b/frontend/app/blog/layout.tsx new file mode 100644 index 00000000..f5fc05fe --- /dev/null +++ b/frontend/app/blog/layout.tsx @@ -0,0 +1,27 @@ +import { HomeLayout } from 'fumadocs-ui/layouts/home'; +import type { ReactNode } from 'react'; + +export default function BlogLayout({ children }: { children: ReactNode }) { + return ( + + cloudemu + + ), + url: '/', + }} + links={[ + { text: 'Docs', url: '/docs' }, + { text: 'Blog', url: '/blog' }, + { + text: 'GitHub', + url: 'https://github.com/stackshy/cloudemu', + }, + ]} + > + {children} + + ); +} diff --git a/frontend/app/blog/page.tsx b/frontend/app/blog/page.tsx new file mode 100644 index 00000000..e87c9045 --- /dev/null +++ b/frontend/app/blog/page.tsx @@ -0,0 +1,36 @@ +import Link from 'next/link'; + +const posts = [ + { + title: 'Introducing cloudemu', + description: 'Zero-cost in-memory cloud emulation for Go', + date: '2026-03-26', + slug: 'hello-world', + }, +]; + +export default function BlogPage() { + return ( +
    +

    Blog

    +

    + Updates, tutorials, and insights from the cloudemu team +

    +
    + {posts.map((post) => ( + + +

    + {post.title} +

    +

    {post.description}

    + + ))} +
    +
    + ); +} diff --git a/frontend/app/docs/[[...slug]]/page.tsx b/frontend/app/docs/[[...slug]]/page.tsx new file mode 100644 index 00000000..efb6ac84 --- /dev/null +++ b/frontend/app/docs/[[...slug]]/page.tsx @@ -0,0 +1,48 @@ +import { source } from '@/lib/source'; +import { + DocsPage, + DocsBody, + DocsTitle, + DocsDescription, +} from 'fumadocs-ui/page'; +import { notFound } from 'next/navigation'; +import defaultMdxComponents from 'fumadocs-ui/mdx'; +import type { Metadata } from 'next'; + +export default async function Page(props: { + params: Promise<{ slug?: string[] }>; +}) { + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) notFound(); + + const data = page.data as any; + const MDX = data.body; + + return ( + + {data.title} + {data.description} + + + + + ); +} + +export async function generateStaticParams() { + return source.generateParams(); +} + +export async function generateMetadata(props: { + params: Promise<{ slug?: string[] }>; +}): Promise { + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) notFound(); + + return { + title: page.data.title, + description: page.data.description, + }; +} diff --git a/frontend/app/docs/layout.tsx b/frontend/app/docs/layout.tsx new file mode 100644 index 00000000..fc8119c8 --- /dev/null +++ b/frontend/app/docs/layout.tsx @@ -0,0 +1,30 @@ +import { DocsLayout } from 'fumadocs-ui/layouts/docs'; +import type { ReactNode } from 'react'; +import { source } from '@/lib/source'; + +export default function Layout({ children }: { children: ReactNode }) { + return ( + + cloudemu + + ), + url: '/', + }} + sidebar={{ + defaultOpenLevel: 1, + }} + links={[ + { + text: 'Blog', + url: '/blog', + }, + ]} + > + {children} + + ); +} diff --git a/frontend/app/global.css b/frontend/app/global.css new file mode 100644 index 00000000..1519fb06 --- /dev/null +++ b/frontend/app/global.css @@ -0,0 +1,9 @@ +@import 'tailwindcss'; +@import 'fumadocs-ui/css/neutral.css'; +@import 'fumadocs-ui/css/preset.css'; + +@theme { + --color-aws: #FF9900; + --color-azure: #0078D4; + --color-gcp: #4285F4; +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 00000000..21981ea7 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,41 @@ +import './global.css'; +import { RootProvider } from 'fumadocs-ui/provider/next'; +import type { ReactNode } from 'react'; +import type { Metadata } from 'next'; +import CustomSearchDialog from '@/components/search-dialog'; + +export const metadata: Metadata = { + title: { + template: '%s | cloudemu', + default: 'cloudemu — Zero-Cost Cloud Emulation for Go', + }, + description: + 'In-memory cloud service emulation for AWS, Azure, and GCP. No cloud accounts, no Docker, no network calls.', + openGraph: { + title: 'cloudemu — Zero-Cost Cloud Emulation for Go', + description: + 'In-memory cloud service emulation for AWS, Azure, and GCP. No cloud accounts, no Docker, no network calls.', + siteName: 'cloudemu', + }, +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + + {children} + + + + ); +} diff --git a/frontend/components/landing/code-example.tsx b/frontend/components/landing/code-example.tsx new file mode 100644 index 00000000..767a15a3 --- /dev/null +++ b/frontend/components/landing/code-example.tsx @@ -0,0 +1,110 @@ +'use client'; + +import { useState } from 'react'; + +const tabs = [ + { + label: 'AWS', + color: '#FF9900', + code: `aws := cloudemu.NewAWS() + +// Launch EC2 instances +instances, _ := aws.EC2.RunInstances(ctx, computedriver.InstanceConfig{ + ImageID: "ami-0abcdef1234", + InstanceType: "t3.large", + Tags: map[string]string{"env": "production"}, +}, 3) + +// Create S3 bucket and upload +aws.S3.CreateBucket(ctx, "app-data") +aws.S3.PutObject(ctx, "app-data", "config.yaml", + []byte("port: 8080"), "text/yaml", nil) + +// Push metrics to CloudWatch +aws.CloudWatch.PutMetricData(ctx, []mondriver.MetricDatum{ + {Namespace: "App", MetricName: "CPU", Value: 45.2}, +})`, + }, + { + label: 'Azure', + color: '#0078D4', + code: `azure := cloudemu.NewAzure() + +// Launch Virtual Machines +instances, _ := azure.VirtualMachines.RunInstances(ctx, + computedriver.InstanceConfig{ + ImageID: "Ubuntu-22.04", + InstanceType: "Standard_D2s_v3", + Tags: map[string]string{"env": "production"}, + }, 3) + +// Create Blob Storage container +azure.BlobStorage.CreateBucket(ctx, "app-data") +azure.BlobStorage.PutObject(ctx, "app-data", "config.yaml", + []byte("port: 8080"), "text/yaml", nil) + +// Push metrics to Azure Monitor +azure.Monitor.PutMetricData(ctx, []mondriver.MetricDatum{ + {Namespace: "App", MetricName: "CPU", Value: 45.2}, +})`, + }, + { + label: 'GCP', + color: '#4285F4', + code: `gcp := cloudemu.NewGCP() + +// Launch GCE instances +instances, _ := gcp.GCE.RunInstances(ctx, + computedriver.InstanceConfig{ + ImageID: "debian-11", + InstanceType: "e2-standard-2", + Tags: map[string]string{"env": "production"}, + }, 3) + +// Create GCS bucket +gcp.GCS.CreateBucket(ctx, "app-data") +gcp.GCS.PutObject(ctx, "app-data", "config.yaml", + []byte("port: 8080"), "text/yaml", nil) + +// Push metrics to Cloud Monitoring +gcp.CloudMonitoring.PutMetricData(ctx, []mondriver.MetricDatum{ + {Namespace: "App", MetricName: "CPU", Value: 45.2}, +})`, + }, +]; + +export function CodeExample() { + const [active, setActive] = useState(0); + + return ( +
    +

    Same API, Every Provider

    +

    + Switch providers by changing one line — your test code stays the same +

    +
    +
    + {tabs.map((tab, i) => ( + + ))} +
    +
    +          
    +            {tabs[active].code}
    +          
    +        
    +
    +
    + ); +} diff --git a/frontend/components/landing/comparison-table.tsx b/frontend/components/landing/comparison-table.tsx new file mode 100644 index 00000000..55382474 --- /dev/null +++ b/frontend/components/landing/comparison-table.tsx @@ -0,0 +1,117 @@ +import { Check, X, Minus } from 'lucide-react'; + +export function ComparisonTable() { + return ( +
    +

    Why cloudemu?

    +

    + Compare approaches to testing cloud-dependent code +

    +
    + + + + + + + + + + + + + + + + + + +
    FeatureReal CloudLocalStack / Emulators + cloudemu +
    +
    +
    + ); +} + +function Row({ + feature, + real, + emulator, + cloudemu, + highlight, + invertBool, +}: { + feature: string; + real: string | boolean; + emulator: string | boolean; + cloudemu: string | boolean; + highlight?: boolean; + invertBool?: boolean; +}) { + const renderCell = (value: string | boolean, isCloudemu = false) => { + if (typeof value === 'boolean') { + const good = invertBool ? !value : value; + return good ? ( + + ) : ( + + ); + } + return ( + + {value} + + ); + }; + + return ( + + {feature} + {renderCell(real)} + {renderCell(emulator)} + + {renderCell(cloudemu, true)} + + + ); +} diff --git a/frontend/components/landing/cta-section.tsx b/frontend/components/landing/cta-section.tsx new file mode 100644 index 00000000..df4bb9ec --- /dev/null +++ b/frontend/components/landing/cta-section.tsx @@ -0,0 +1,60 @@ +'use client'; + +import Link from 'next/link'; +import { useState } from 'react'; +import { Copy, Check } from 'lucide-react'; + +export function CTASection() { + const [copied, setCopied] = useState(false); + const installCmd = 'go get github.com/stackshy/cloudemu'; + + const handleCopy = async () => { + await navigator.clipboard.writeText(installCmd); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
    +

    Ready to get started?

    +

    + Install cloudemu and start testing your cloud code in seconds +

    + +
    + $ + {installCmd} + +
    + +
    + + Read the Docs + + + Quick Start Guide + +
    + +

    + MIT License · Requires Go 1.25+ +

    +
    + ); +} diff --git a/frontend/components/landing/feature-cards.tsx b/frontend/components/landing/feature-cards.tsx new file mode 100644 index 00000000..616ed0ce --- /dev/null +++ b/frontend/components/landing/feature-cards.tsx @@ -0,0 +1,74 @@ +import { + Workflow, + Activity, + AlertTriangle, + Video, + Clock, + Package, +} from 'lucide-react'; + +const features = [ + { + title: 'State Machines', + description: + 'VMs enforce valid lifecycle transitions. Illegal state changes return errors, just like real clouds.', + icon: Workflow, + }, + { + title: 'Auto-Metrics', + description: + 'Launching a VM pushes CPU, Network, and Disk metrics to monitoring. Stop/terminate emit matching values.', + icon: Activity, + }, + { + title: 'Error Injection', + description: + 'Simulate failures deterministically: always, every Nth call, probabilistic, or first N calls.', + icon: AlertTriangle, + }, + { + title: 'Call Recording', + description: + 'Capture every API call with inputs, outputs, errors, and timing. Assert with fluent matchers.', + icon: Video, + }, + { + title: 'Fake Clock', + description: + 'Control time for deterministic testing of TTL, deduplication windows, and alarm evaluation.', + icon: Clock, + }, + { + title: 'Zero Dependencies', + description: + 'Pure Go with no external dependencies. Only testify for tests. Works anywhere Go runs.', + icon: Package, + }, +]; + +export function FeatureCards() { + return ( +
    +

    Beyond Basic Mocks

    +

    + cloudemu reproduces real cloud behaviors so your tests catch real issues +

    +
    + {features.map((feature) => ( +
    +
    + +
    +

    {feature.title}

    +

    + {feature.description} +

    +
    + ))} +
    +
    + ); +} diff --git a/frontend/components/landing/hero.tsx b/frontend/components/landing/hero.tsx new file mode 100644 index 00000000..063bc3d0 --- /dev/null +++ b/frontend/components/landing/hero.tsx @@ -0,0 +1,98 @@ +'use client'; + +import Link from 'next/link'; +import { motion } from 'framer-motion'; + +export function Hero() { + return ( +
    + +
    + + Open Source · MIT License +
    + +

    + Zero-Cost{' '} + + Cloud Emulation + +
    + for Go +

    + +

    + No cloud accounts. No Docker. No network calls. +
    + Just go get and test. +

    + +
    + + Get Started + + + GitHub + +
    +
    + + +
    +
    + + + + main.go +
    +
    +            
    +              {'// Create cloud providers — everything runs in memory'}
    +              {'\n'}
    +              aws
    +              {' := '}
    +              cloudemu
    +              .NewAWS()
    +              {'\n'}
    +              azure
    +              {' := '}
    +              cloudemu
    +              .NewAzure()
    +              {'\n'}
    +              gcp
    +              {' := '}
    +              cloudemu
    +              .NewGCP()
    +              {'\n\n'}
    +              {'// Use them exactly like real cloud SDKs'}
    +              {'\n'}
    +              instances
    +              {', _ := '}
    +              aws
    +              .EC2.RunInstances(ctx, config, 
    +              3
    +              )
    +            
    +          
    +
    +
    +
    + ); +} diff --git a/frontend/components/landing/service-grid.tsx b/frontend/components/landing/service-grid.tsx new file mode 100644 index 00000000..259b1e22 --- /dev/null +++ b/frontend/components/landing/service-grid.tsx @@ -0,0 +1,55 @@ +import Link from 'next/link'; +import { + Server, HardDrive, Database, Zap, Network, Activity, + Shield, Globe, GitBranch, MessageSquare, Bell, Radio, + Box, MemoryStick, Lock, FileText, +} from 'lucide-react'; +import { services } from '@/lib/services'; + +const iconMap: Record> = { + Server, HardDrive, Database, Zap, Network, Activity, + Shield, Globe, GitBranch, MessageSquare, Bell, Radio, + Box, MemoryStick, Lock, FileText, +}; + +export function ServiceGrid() { + return ( +
    +

    16 Service Categories

    +

    + Every category is implemented for AWS, Azure, and GCP +

    +
    + {services.map((service) => { + const Icon = iconMap[service.icon]; + return ( + +
    + {Icon && } +

    {service.category}

    +
    +
    + + AWS{' '} + {service.aws} + + + Azure{' '} + {service.azure} + + + GCP{' '} + {service.gcp} + +
    + + ); + })} +
    +
    + ); +} diff --git a/frontend/components/search-dialog.tsx b/frontend/components/search-dialog.tsx new file mode 100644 index 00000000..51cc8291 --- /dev/null +++ b/frontend/components/search-dialog.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { useDocsSearch } from 'fumadocs-core/search/client'; +import { + SearchDialog, + SearchDialogOverlay, + SearchDialogContent, + SearchDialogHeader, + SearchDialogIcon, + SearchDialogInput, + SearchDialogClose, + SearchDialogList, + SearchDialogFooter, +} from 'fumadocs-ui/components/dialog/search'; +import type { SharedProps } from 'fumadocs-ui/contexts/search'; + +export default function CustomSearchDialog(props: SharedProps) { + const { search, setSearch, query } = useDocsSearch({ + type: 'fetch', + api: '/api/search', + }); + + const items = + query.data === 'empty' + ? null + : query.data && query.data.length > 0 + ? query.data + : null; + + return ( + + + + + + + + + + search.length > 0 ? ( +
    + No results found for "{search}" +
    + ) : ( +
    + Type to search documentation... +
    + ) + } + /> +
    + +
    + ); +} diff --git a/frontend/content/blog/hello-world.mdx b/frontend/content/blog/hello-world.mdx new file mode 100644 index 00000000..53e90d7f --- /dev/null +++ b/frontend/content/blog/hello-world.mdx @@ -0,0 +1,50 @@ +--- +title: Introducing cloudemu +description: Zero-cost in-memory cloud emulation for Go +--- + +# Introducing cloudemu + +We're excited to announce **cloudemu** — a Go library that emulates AWS, Azure, and GCP cloud services entirely in memory. + +## The Problem + +Every team that builds on cloud services faces the same testing dilemma: + +1. **Use real cloud accounts** — expensive, slow, requires network access +2. **Use Docker-based emulators** — complex setup, moderate speed, heavy on resources +3. **Write custom mocks** — tedious, incomplete, hard to maintain + +None of these options give you fast, realistic, and free cloud testing. + +## The Solution + +cloudemu provides 48 in-memory service implementations (16 categories across AWS, Azure, and GCP) that behave like the real thing: + +```go +aws := cloudemu.NewAWS() + +// This works exactly like real EC2 +instances, _ := aws.EC2.RunInstances(ctx, config, 3) +``` + +No cloud accounts. No Docker. No network calls. Tests run in ~10ms. + +## What Makes It Different + +cloudemu goes beyond basic CRUD mocks: + +- **State machines** enforce valid lifecycle transitions +- **Auto-metrics** are emitted to the monitoring service +- **FIFO deduplication** with 5-minute windows +- **Dead-letter queues** for messages exceeding max receive count +- **TTL expiry** with lazy cleanup on read +- **IAM policy evaluation** with wildcard matching + +## Get Started + +```bash +go get github.com/stackshy/cloudemu +``` + +Check out the [Quick Start guide](/docs/quick-start) to build your first cloud simulation in 5 minutes. diff --git a/frontend/content/docs/architecture.mdx b/frontend/content/docs/architecture.mdx new file mode 100644 index 00000000..730f578b --- /dev/null +++ b/frontend/content/docs/architecture.mdx @@ -0,0 +1,91 @@ +--- +title: Architecture +description: Three-layer design inspired by Go CDK +--- + +# Architecture + +cloudemu follows a three-layer architecture inspired by [Go CDK](https://gocloud.dev/): + +``` +Portable API → recording, metrics, rate limiting, error injection +Driver Interface → minimal Go interfaces per service +Provider Mocks → in-memory backends (AWS/Azure/GCP) using generic memstore +``` + +## Layer 1: Provider Mocks + +The bottom layer contains the actual in-memory implementations for each cloud provider. Each provider (AWS, Azure, GCP) implements all 16 service interfaces. + +```go +// providers/aws/aws.go +type Provider struct { + S3 *s3.Mock + EC2 *ec2.Mock + DynamoDB *dynamodb.Mock + Lambda *lambda.Mock + CloudWatch *cloudwatch.Mock + // ... 16 services total +} +``` + +All mocks are backed by a generic, thread-safe `memstore.Store[V]` — a simple in-memory key-value store with `Get`, `Set`, `Delete`, `Filter`, and more. + +Services are wired together at initialization. For example, EC2 is connected to CloudWatch so that launching instances automatically emits CPU, Network, and Disk metrics. + +## Layer 2: Driver Interfaces + +Each service category defines a minimal Go interface that all providers must implement: + +```go +// compute/driver/driver.go +type Compute interface { + RunInstances(ctx context.Context, config InstanceConfig, count int) ([]Instance, error) + StopInstances(ctx context.Context, instanceIDs []string) error + TerminateInstances(ctx context.Context, instanceIDs []string) error + DescribeInstances(ctx context.Context, ids []string, filters []DescribeFilter) ([]Instance, error) + // ... +} +``` + +The driver layer ensures that AWS S3, Azure Blob Storage, and GCP GCS all satisfy the same `driver.Bucket` interface. Your code can work with any of them interchangeably. + +## Layer 3: Portable API + +The top layer wraps driver implementations with cross-cutting concerns: + +```go +bucket := storage.NewBucket(aws.S3, + storage.WithRecorder(rec), // record every API call + storage.WithMetrics(mc), // track call counts and durations + storage.WithErrorInjection(inj), // simulate cloud failures + storage.WithRateLimiter(limiter), // simulate API throttling + storage.WithLatency(5*time.Millisecond),// simulate network delay +) +``` + +The portable API intercepts every call and applies the configured concerns in order: +1. **Error injection** — check if this call should fail +2. **Rate limiting** — check if rate limit is exceeded +3. **Latency** — sleep for configured duration +4. **Execute** — call the underlying driver +5. **Metrics** — record call count, duration, errors +6. **Recording** — log the call details + +## Key Design Decisions + +### Generic memstore + +All providers share the same `memstore.Store[V]` for storage. This ensures consistent thread-safety and behavior across providers. + +### Monitoring auto-wiring + +When a provider is created, services that produce metrics (like EC2) are automatically connected to the monitoring service (like CloudWatch). You can query these metrics the same way you would in production. + +### State machines + +Services with lifecycle states (compute, serverless) use formal state machines that enforce valid transitions. Invalid transitions return `FailedPrecondition` errors. + +### Zero dependencies + +cloudemu has no external dependencies beyond the Go standard library. The only test dependency is `testify`. diff --git a/frontend/content/docs/configuration.mdx b/frontend/content/docs/configuration.mdx new file mode 100644 index 00000000..cd005499 --- /dev/null +++ b/frontend/content/docs/configuration.mdx @@ -0,0 +1,109 @@ +--- +title: Configuration +description: Configure cloudemu providers with regions, clocks, latency, and more +--- + +# Configuration + +All three providers accept the same functional options from the `config` package. + +```go +import "github.com/stackshy/cloudemu/config" +``` + +## Available Options + +### WithRegion + +Set the cloud region. Defaults to `"us-east-1"`. + +```go +aws := cloudemu.NewAWS( + config.WithRegion("eu-west-1"), +) +``` + +### WithAccountID + +Set the cloud account ID. Defaults to `"123456789012"`. + +```go +aws := cloudemu.NewAWS( + config.WithAccountID("999888777666"), +) +``` + +### WithProjectID + +Set the GCP project ID. Defaults to `"mock-project"`. + +```go +gcp := cloudemu.NewGCP( + config.WithProjectID("my-project-123"), +) +``` + +### WithLatency + +Add simulated network latency to all operations. + +```go +aws := cloudemu.NewAWS( + config.WithLatency(50 * time.Millisecond), +) +``` + +### WithClock + +Control time for deterministic testing. By default, cloudemu uses `RealClock` (system time). + +```go +// Create a fake clock starting at a specific time +clock := config.NewFakeClock(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) + +aws := cloudemu.NewAWS( + config.WithClock(clock), +) + +// Advance time programmatically +clock.Advance(5 * time.Minute) + +// Or set to a specific time +clock.Set(time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)) +``` + +The fake clock is thread-safe and useful for testing: +- TTL expiry in databases and caches +- FIFO deduplication windows in message queues +- Alarm evaluation in monitoring +- Lifecycle policy evaluation in storage + +## Combining Options + +Pass multiple options to any provider: + +```go +clock := config.NewFakeClock(time.Now()) + +aws := cloudemu.NewAWS( + config.WithRegion("eu-west-1"), + config.WithAccountID("999888777666"), + config.WithClock(clock), + config.WithLatency(5 * time.Millisecond), +) + +azure := cloudemu.NewAzure( + config.WithRegion("westeurope"), + config.WithClock(clock), // share the same clock across providers +) +``` + +## Defaults + +| Option | Default Value | +|--------|--------------| +| Region | `"us-east-1"` | +| AccountID | `"123456789012"` | +| ProjectID | `"mock-project"` | +| Clock | `RealClock{}` (system time) | +| Latency | `0` (no delay) | diff --git a/frontend/content/docs/error-handling.mdx b/frontend/content/docs/error-handling.mdx new file mode 100644 index 00000000..e0a890a1 --- /dev/null +++ b/frontend/content/docs/error-handling.mdx @@ -0,0 +1,91 @@ +--- +title: Error Handling +description: Canonical error codes and helper functions in cloudemu +--- + +# Error Handling + +All cloudemu operations return errors with canonical codes. Import the errors package: + +```go +import cerrors "github.com/stackshy/cloudemu/errors" +``` + +## Error Codes + +| Code | Description | +|------|-------------| +| `OK` | No error | +| `NotFound` | Resource does not exist | +| `AlreadyExists` | Resource already exists | +| `InvalidArgument` | Invalid input parameter | +| `FailedPrecondition` | Operation rejected due to current state (e.g., invalid state transition) | +| `PermissionDenied` | IAM policy denies the action | +| `Throttled` | Rate limit exceeded | +| `Internal` | Internal error | +| `Unimplemented` | Operation not implemented | +| `ResourceExhausted` | Resource quota exceeded | +| `Unavailable` | Service temporarily unavailable | + +## Checking Error Types + +Use the helper functions to check specific error types: + +```go +_, err := aws.S3.GetObject(ctx, "bucket", "missing-key") +if cerrors.IsNotFound(err) { + // Handle missing resource +} + +err = aws.S3.CreateBucket(ctx, "existing-bucket") +if cerrors.IsAlreadyExists(err) { + // Bucket already exists +} +``` + +### Available Helpers + +```go +cerrors.IsNotFound(err) bool +cerrors.IsAlreadyExists(err) bool +cerrors.IsThrottled(err) bool +cerrors.IsInvalidArgument(err) bool +cerrors.IsFailedPrecondition(err) bool +cerrors.IsPermissionDenied(err) bool +``` + +## Extracting Error Codes + +For error codes without dedicated helpers, use `GetCode`: + +```go +code := cerrors.GetCode(err) + +switch code { +case cerrors.NotFound: + // handle not found +case cerrors.Throttled: + // handle rate limiting +case cerrors.ResourceExhausted: + // handle quota exceeded +default: + // handle other errors +} +``` + +`GetCode` returns: +- `OK` for `nil` errors +- The cloudemu error code for `*cerrors.Error` values +- `Internal` for any other error type + +## Error Format + +Error messages follow the pattern `"Code: message"`: + +```go +err := cerrors.New(cerrors.NotFound, "bucket 'my-bucket' not found") +fmt.Println(err) // "NotFound: bucket 'my-bucket' not found" + +err = cerrors.Newf(cerrors.InvalidArgument, "key %q is empty", "") +fmt.Println(err) // "InvalidArgument: key \"\" is empty" +``` diff --git a/frontend/content/docs/features/error-injection.mdx b/frontend/content/docs/features/error-injection.mdx new file mode 100644 index 00000000..72a7d4c2 --- /dev/null +++ b/frontend/content/docs/features/error-injection.mdx @@ -0,0 +1,71 @@ +--- +title: Error Injection +description: Simulate cloud failures deterministically +--- + +# Error Injection + +Simulate various failure modes to test your error handling and resilience. + +## Setup + +```go +import "github.com/stackshy/cloudemu/inject" + +inj := inject.NewInjector() + +bucket := storage.NewBucket(aws.S3, + storage.WithErrorInjection(inj), +) +``` + +## Failure Policies + +### Always Fail + +```go +inj.SetPolicy("storage", "PutObject", inject.AlwaysFail( + cerrors.New(cerrors.Unavailable, "service unavailable"), +)) +``` + +### Every Nth Call + +```go +// Fail every 3rd call +inj.SetPolicy("storage", "PutObject", inject.EveryN(3, + cerrors.New(cerrors.Internal, "intermittent failure"), +)) +``` + +### Probabilistic + +```go +// 10% chance of failure +inj.SetPolicy("storage", "PutObject", inject.Probabilistic(0.1, + cerrors.New(cerrors.Internal, "random failure"), +)) +``` + +### First N Calls + +```go +// First 2 calls fail, rest succeed +inj.SetPolicy("storage", "PutObject", inject.FirstN(2, + cerrors.New(cerrors.Unavailable, "warming up"), +)) +``` + +## Clear Policies + +```go +inj.ClearPolicy("storage", "PutObject") +inj.ClearAll() +``` + +## Use Cases + +- Test retry logic with intermittent failures +- Verify timeout handling when services are unavailable +- Simulate cold start failures +- Test circuit breaker patterns diff --git a/frontend/content/docs/features/fake-clock.mdx b/frontend/content/docs/features/fake-clock.mdx new file mode 100644 index 00000000..84ac3c05 --- /dev/null +++ b/frontend/content/docs/features/fake-clock.mdx @@ -0,0 +1,86 @@ +--- +title: Fake Clock +description: Deterministic time control for TTL, dedup, and alarms +--- + +# Fake Clock + +Control time programmatically for deterministic testing. + +## Setup + +```go +import "github.com/stackshy/cloudemu/config" + +clock := config.NewFakeClock(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) + +aws := cloudemu.NewAWS( + config.WithClock(clock), +) +``` + +## Manipulating Time + +```go +// Move forward by a duration +clock.Advance(5 * time.Minute) + +// Jump to a specific time +clock.Set(time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)) + +// Check current fake time +now := clock.Now() +``` + +## Use Cases + +### TTL Expiry + +```go +clock := config.NewFakeClock(time.Now()) +aws := cloudemu.NewAWS(config.WithClock(clock)) + +// Set item with TTL +aws.DynamoDB.PutItem(ctx, "sessions", map[string]any{ + "id": "s-1", "expiresAt": clock.Now().Add(1 * time.Hour).Unix(), +}) + +// Item exists now +item, _ := aws.DynamoDB.GetItem(ctx, "sessions", map[string]any{"id": "s-1"}) +// item != nil + +// Advance past TTL +clock.Advance(2 * time.Hour) + +// Item is now expired +item, _ = aws.DynamoDB.GetItem(ctx, "sessions", map[string]any{"id": "s-1"}) +// item == nil +``` + +### FIFO Deduplication + +```go +// Send a message +aws.SQS.SendMessage(ctx, "queue.fifo", mqdriver.SendMessageInput{ + Body: "order-1", MessageDeduplicationId: "dedup-1", +}) + +// Same dedup ID within 5 minutes — deduplicated +aws.SQS.SendMessage(ctx, "queue.fifo", mqdriver.SendMessageInput{ + Body: "order-1", MessageDeduplicationId: "dedup-1", +}) +// Only 1 message in queue + +// Advance past dedup window +clock.Advance(6 * time.Minute) + +// Now the same ID can be used again +aws.SQS.SendMessage(ctx, "queue.fifo", mqdriver.SendMessageInput{ + Body: "order-1", MessageDeduplicationId: "dedup-1", +}) +// 2 messages in queue +``` + +## Thread Safety + +`FakeClock` is thread-safe — it uses a mutex internally. You can safely share one clock across multiple goroutines and providers. diff --git a/frontend/content/docs/features/index.mdx b/frontend/content/docs/features/index.mdx new file mode 100644 index 00000000..06c117f2 --- /dev/null +++ b/frontend/content/docs/features/index.mdx @@ -0,0 +1,29 @@ +--- +title: Cross-Cutting Features +description: Recording, metrics, rate limiting, error injection, and more +--- + +# Cross-Cutting Features + +Every cloudemu service can be wrapped with a portable API layer that adds test-oriented features. These features compose together and work identically across all providers. + +```go +bucket := storage.NewBucket(aws.S3, + storage.WithRecorder(rec), // record every API call + storage.WithMetrics(mc), // track call counts and durations + storage.WithErrorInjection(inj), // simulate cloud failures + storage.WithRateLimiter(limiter), // simulate API throttling + storage.WithLatency(5*time.Millisecond),// simulate network delay +) +``` + +## Available Features + +| Feature | What It Does | +|---------|-------------| +| [Call Recording](/docs/features/recording) | Capture every API call with inputs, outputs, errors, and timing | +| [Metrics](/docs/features/metrics) | Track `calls_total`, `call_duration`, `errors_total` per operation | +| [Rate Limiting](/docs/features/rate-limiting) | Token bucket limiter that returns `Throttled` errors when exhausted | +| [Error Injection](/docs/features/error-injection) | Simulate failures: always, every Nth call, probabilistic, or first N calls | +| [Fake Clock](/docs/features/fake-clock) | Control time for deterministic testing of TTL, dedup, alarms | +| [Latency Simulation](/docs/features/latency-simulation) | Add delays to test timeout handling | diff --git a/frontend/content/docs/features/latency-simulation.mdx b/frontend/content/docs/features/latency-simulation.mdx new file mode 100644 index 00000000..6b9dd909 --- /dev/null +++ b/frontend/content/docs/features/latency-simulation.mdx @@ -0,0 +1,50 @@ +--- +title: Latency Simulation +description: Add delays to test timeout handling and performance +--- + +# Latency Simulation + +Add artificial latency to simulate real-world network conditions. + +## Provider-Level Latency + +Add latency to all operations on a provider: + +```go +aws := cloudemu.NewAWS( + config.WithLatency(50 * time.Millisecond), +) +``` + +## Service-Level Latency + +Add latency to specific services using the portable API: + +```go +bucket := storage.NewBucket(aws.S3, + storage.WithLatency(100 * time.Millisecond), +) +``` + +## Use Cases + +- Test timeout handling — verify your code handles slow responses correctly +- Test context cancellation — ensure operations respect `context.WithTimeout` +- Performance testing — measure how your code behaves under various latency conditions + +```go +ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) +defer cancel() + +// This will succeed (100ms < 200ms timeout) +bucket.PutObject(ctx, "bucket", "key", data, "text/plain", nil) + +// With higher latency, it would timeout +slowBucket := storage.NewBucket(aws.S3, + storage.WithLatency(500 * time.Millisecond), +) + +err := slowBucket.PutObject(ctx, "bucket", "key2", data, "text/plain", nil) +// err == context.DeadlineExceeded +``` diff --git a/frontend/content/docs/features/meta.json b/frontend/content/docs/features/meta.json new file mode 100644 index 00000000..28f456e9 --- /dev/null +++ b/frontend/content/docs/features/meta.json @@ -0,0 +1,12 @@ +{ + "title": "Cross-Cutting Features", + "pages": [ + "index", + "recording", + "metrics", + "rate-limiting", + "error-injection", + "fake-clock", + "latency-simulation" + ] +} diff --git a/frontend/content/docs/features/metrics.mdx b/frontend/content/docs/features/metrics.mdx new file mode 100644 index 00000000..540e899f --- /dev/null +++ b/frontend/content/docs/features/metrics.mdx @@ -0,0 +1,54 @@ +--- +title: Metrics Collection +description: Track call counts, durations, and errors per operation +--- + +# Metrics Collection + +Collect metrics about API calls for monitoring and assertions. + +## Setup + +```go +import "github.com/stackshy/cloudemu/metrics" + +mc := &metrics.Collector{} + +bucket := storage.NewBucket(aws.S3, + storage.WithMetrics(mc), +) +``` + +## Automatic Metrics + +Every operation automatically records: +- `calls_total` — counter incremented on each call +- `call_duration` — histogram of call durations +- `errors_total` — counter incremented on errors + +All metrics include labels: `service` and `operation`. + +## Querying Metrics + +```go +// Get all counters +counters := mc.Query(metrics.Query{ + Type: metrics.CounterType, + Name: "calls_total", +}) + +// Filter by labels +storagePuts := mc.Query(metrics.Query{ + Type: metrics.CounterType, + Name: "calls_total", + Labels: map[string]string{"operation": "PutObject"}, +}) +``` + +## Metric Types + +| Type | Description | +|------|-------------| +| `CounterType` | Monotonically increasing counter | +| `GaugeType` | Value that can go up and down | +| `HistogramType` | Distribution of values (durations) | diff --git a/frontend/content/docs/features/rate-limiting.mdx b/frontend/content/docs/features/rate-limiting.mdx new file mode 100644 index 00000000..06011ad1 --- /dev/null +++ b/frontend/content/docs/features/rate-limiting.mdx @@ -0,0 +1,39 @@ +--- +title: Rate Limiting +description: Token bucket rate limiter that returns Throttled errors +--- + +# Rate Limiting + +Simulate API rate limits using a token bucket algorithm. + +## Setup + +```go +import "github.com/stackshy/cloudemu/ratelimit" + +limiter := ratelimit.NewLimiter(100) // 100 requests per second + +bucket := storage.NewBucket(aws.S3, + storage.WithRateLimiter(limiter), +) +``` + +## Behavior + +When the rate limit is exceeded, operations return a `Throttled` error: + +```go +import cerrors "github.com/stackshy/cloudemu/errors" + +err := bucket.PutObject(ctx, "bucket", "key", data, "text/plain", nil) +if cerrors.IsThrottled(err) { + // Rate limit exceeded — back off and retry +} +``` + +## Use Cases + +- Test that your retry logic handles rate limiting correctly +- Verify circuit breaker behavior under sustained throttling +- Simulate cloud provider API quotas diff --git a/frontend/content/docs/features/recording.mdx b/frontend/content/docs/features/recording.mdx new file mode 100644 index 00000000..501eddf9 --- /dev/null +++ b/frontend/content/docs/features/recording.mdx @@ -0,0 +1,65 @@ +--- +title: Call Recording +description: Capture every API call with inputs, outputs, errors, and timing +--- + +# Call Recording + +Record every API call made to a service for test assertions. + +## Setup + +```go +import "github.com/stackshy/cloudemu/recorder" + +rec := &recorder.Recorder{} + +bucket := storage.NewBucket(aws.S3, + storage.WithRecorder(rec), +) +``` + +## Inspecting Calls + +After running your code, inspect all recorded calls: + +```go +calls := rec.Calls() +for _, call := range calls { + fmt.Printf("%s.%s — duration: %v, error: %v\n", + call.Service, call.Operation, call.Duration, call.Error) +} +``` + +Each `Call` contains: +- `Service` — e.g., "storage", "compute" +- `Operation` — e.g., "PutObject", "RunInstances" +- `Input` — the operation input +- `Output` — the operation output +- `Error` — any error returned +- `Timestamp` — when the call was made +- `Duration` — how long it took + +## Fluent Assertions + +Use the `Matcher` for expressive test assertions: + +```go +// Count calls to a specific operation +count := rec.Matcher().Service("storage").Operation("PutObject").Count() +assert.Equal(t, 3, count) + +// Check that no errors occurred +rec.Matcher().Service("storage").AssertNoErrors(t) + +// Filter by service +storageCalls := rec.Matcher().Service("storage").Calls() +``` + +## Reset + +Clear all recorded calls between tests: + +```go +rec.Reset() +``` diff --git a/frontend/content/docs/index.mdx b/frontend/content/docs/index.mdx new file mode 100644 index 00000000..c4562f35 --- /dev/null +++ b/frontend/content/docs/index.mdx @@ -0,0 +1,59 @@ +--- +title: Introduction +description: cloudemu is a Go library that emulates cloud services entirely in memory +--- + +# Welcome to cloudemu + +**cloudemu** is a Go library that emulates AWS, Azure, and GCP cloud services entirely in memory. No real cloud accounts, no Docker, no network calls — just import the package, create a provider, and test your cloud code instantly. + +## Why cloudemu? + +Testing cloud-dependent code is painful. You either pay for real accounts, wrestle with heavy emulators that need Docker, or write incomplete mocks from scratch. + +| Approach | Cost | Speed | Offline | +|----------|------|-------|---------| +| Real cloud (AWS/Azure/GCP) | $$$ | Slow (seconds) | No | +| LocalStack / Emulators | $ | Medium (100ms+) | Yes | +| **cloudemu** | **Free** | **Fast (~10ms)** | **Yes** | + +## Quick Example + +```go +package main + +import ( + "context" + "fmt" + "github.com/stackshy/cloudemu" + "github.com/stackshy/cloudemu/compute/driver" +) + +func main() { + ctx := context.Background() + aws := cloudemu.NewAWS() + + instances, _ := aws.EC2.RunInstances(ctx, driver.InstanceConfig{ + ImageID: "ami-0abcdef1234567890", + InstanceType: "t2.micro", + }, 2) + + fmt.Println(instances[0].State) // "running" + fmt.Println(instances[0].ID) // "i-00000001" +} +``` + +This works identically across all three providers. Replace `aws.EC2` with `azure.VirtualMachines` or `gcp.GCE`. + +## What's Included + +- **16 service categories** across 3 cloud providers (48 total implementations) +- **Realistic behaviors**: state machines, auto-metrics, alarm evaluation, FIFO dedup, DLQs, TTL expiry +- **Cross-cutting features**: call recording, metrics collection, error injection, rate limiting, fake clock, latency simulation +- **Zero external dependencies** (only `testify` for tests) + +## Next Steps + +- [Prerequisites](/docs/prerequisites) — what you need before starting +- [Installation](/docs/installation) — add cloudemu to your project +- [Quick Start](/docs/quick-start) — build something in 5 minutes diff --git a/frontend/content/docs/installation.mdx b/frontend/content/docs/installation.mdx new file mode 100644 index 00000000..f3d72405 --- /dev/null +++ b/frontend/content/docs/installation.mdx @@ -0,0 +1,81 @@ +--- +title: Installation +description: How to install cloudemu in your Go project +--- + +# Installation + +## Install the Package + +Add cloudemu to your Go module: + +```bash +go get github.com/stackshy/cloudemu +``` + +This installs the library and all service packages. There are no external dependencies beyond the Go standard library. + +## Verify Installation + +Create a simple test file to verify everything works: + +```go title="main_test.go" +package main + +import ( + "context" + "testing" + + "github.com/stackshy/cloudemu" +) + +func TestCloudemu(t *testing.T) { + ctx := context.Background() + aws := cloudemu.NewAWS() + + err := aws.S3.CreateBucket(ctx, "test-bucket") + if err != nil { + t.Fatal(err) + } + + buckets, err := aws.S3.ListBuckets(ctx) + if err != nil { + t.Fatal(err) + } + + if len(buckets) != 1 { + t.Fatalf("expected 1 bucket, got %d", len(buckets)) + } +} +``` + +Run it: + +```bash +go test -v ./... +``` + +If you see `PASS`, cloudemu is ready to use. + +## Import Paths + +The main package and common sub-packages: + +```go +import ( + "github.com/stackshy/cloudemu" // NewAWS, NewAzure, NewGCP + "github.com/stackshy/cloudemu/config" // WithRegion, WithClock, etc. + cerrors "github.com/stackshy/cloudemu/errors" // Error codes and helpers + "github.com/stackshy/cloudemu/compute/driver" // Compute types + "github.com/stackshy/cloudemu/storage/driver" // Storage types + "github.com/stackshy/cloudemu/database/driver" // Database types + "github.com/stackshy/cloudemu/monitoring/driver" // Monitoring types +) +``` + +Each service has its own `driver` sub-package containing the types and interfaces you need. + +## Next Steps + +- [Quick Start](/docs/quick-start) — build a complete example +- [Configuration](/docs/configuration) — customize your providers diff --git a/frontend/content/docs/meta.json b/frontend/content/docs/meta.json new file mode 100644 index 00000000..6d09d73b --- /dev/null +++ b/frontend/content/docs/meta.json @@ -0,0 +1,18 @@ +{ + "title": "Documentation", + "pages": [ + "index", + "prerequisites", + "installation", + "quick-start", + "configuration", + "error-handling", + "architecture", + "---", + "...services", + "---", + "...features", + "---", + "portable-api" + ] +} diff --git a/frontend/content/docs/portable-api.mdx b/frontend/content/docs/portable-api.mdx new file mode 100644 index 00000000..ceb8c310 --- /dev/null +++ b/frontend/content/docs/portable-api.mdx @@ -0,0 +1,92 @@ +--- +title: Portable API +description: Wrap any driver with recording, metrics, rate limiting, and error injection +--- + +# Portable API + +The portable API layer wraps driver implementations with test-oriented cross-cutting concerns. Every service category has its own portable wrapper. + +## Basic Usage + +```go +import ( + "github.com/stackshy/cloudemu/storage" + "github.com/stackshy/cloudemu/recorder" + "github.com/stackshy/cloudemu/metrics" + "github.com/stackshy/cloudemu/inject" + "github.com/stackshy/cloudemu/ratelimit" +) + +rec := &recorder.Recorder{} +mc := &metrics.Collector{} +inj := inject.NewInjector() +limiter := ratelimit.NewLimiter(100) // 100 requests/sec + +bucket := storage.NewBucket(aws.S3, + storage.WithRecorder(rec), + storage.WithMetrics(mc), + storage.WithErrorInjection(inj), + storage.WithRateLimiter(limiter), + storage.WithLatency(5 * time.Millisecond), +) + +// Use bucket exactly like aws.S3 — same interface +bucket.CreateBucket(ctx, "my-bucket") +bucket.PutObject(ctx, "my-bucket", "key", data, "text/plain", nil) +``` + +## How It Works + +Every operation passes through a middleware chain: + +1. **Error Injection** — if configured, checks whether the call should fail +2. **Rate Limiting** — if configured, checks the token bucket +3. **Latency** — if configured, sleeps for the specified duration +4. **Driver Call** — executes the actual operation +5. **Metrics** — records `calls_total`, `call_duration`, `errors_total` +6. **Recording** — logs service, operation, input, output, error, duration + +## Available Wrappers + +Each service has its own portable type: + +| Service | Portable Type | Import | +|---------|--------------|--------| +| Storage | `storage.Bucket` | `cloudemu/storage` | +| Compute | `compute.Compute` | `cloudemu/compute` | +| Database | `database.Database` | `cloudemu/database` | +| Serverless | `serverless.Serverless` | `cloudemu/serverless` | +| Monitoring | `monitoring.Monitoring` | `cloudemu/monitoring` | +| Message Queue | `messagequeue.MessageQueue` | `cloudemu/messagequeue` | +| And more... | | | + +All wrappers support the same set of `With*` options. + +## Cross-Provider Testing + +The portable API enables true cross-provider testing: + +```go +func testStorage(t *testing.T, bucket *storage.Bucket) { + ctx := context.Background() + bucket.CreateBucket(ctx, "test") + bucket.PutObject(ctx, "test", "key", []byte("value"), "text/plain", nil) + + obj, err := bucket.GetObject(ctx, "test", "key") + assert.NoError(t, err) + assert.Equal(t, []byte("value"), obj.Data) +} + +func TestAWS(t *testing.T) { + testStorage(t, storage.NewBucket(cloudemu.NewAWS().S3)) +} + +func TestAzure(t *testing.T) { + testStorage(t, storage.NewBucket(cloudemu.NewAzure().BlobStorage)) +} + +func TestGCP(t *testing.T) { + testStorage(t, storage.NewBucket(cloudemu.NewGCP().GCS)) +} +``` diff --git a/frontend/content/docs/prerequisites.mdx b/frontend/content/docs/prerequisites.mdx new file mode 100644 index 00000000..05e9f799 --- /dev/null +++ b/frontend/content/docs/prerequisites.mdx @@ -0,0 +1,69 @@ +--- +title: Prerequisites +description: What you need before using cloudemu +--- + +# Prerequisites + +Before you start using cloudemu, make sure you have the following set up on your machine. + +## Required + +### Go 1.25+ + +cloudemu requires **Go 1.25.0 or later**. Check your version: + +```bash +go version +``` + +If you need to install or upgrade Go, visit [go.dev/dl](https://go.dev/dl/). + +### A Go Module + +Your project should be using Go modules. If you're starting fresh: + +```bash +mkdir my-project && cd my-project +go mod init github.com/yourname/my-project +``` + +## Recommended Knowledge + +### Go Fundamentals + +You should be comfortable with: + +- Writing and running Go programs +- Using `context.Context` for API calls +- Go interfaces and structs +- Error handling patterns (`if err != nil`) +- Go modules and dependency management + +### Cloud Concepts + +While cloudemu handles everything in memory, familiarity with these cloud concepts will help: + +- **Compute**: Virtual machines, instance types, lifecycle states +- **Storage**: Buckets, objects, keys, content types +- **Database**: NoSQL key-value stores, partition keys, queries +- **Networking**: VPCs, subnets, security groups, CIDR blocks +- **Monitoring**: Metrics, alarms, thresholds +- **IAM**: Users, roles, policies, permissions + +You do **not** need: +- A cloud account (AWS, Azure, or GCP) +- Docker or any container runtime +- Network access or internet connection +- Any cloud CLI tools or SDKs + +## Verify Your Setup + +Run this to confirm Go is ready: + +```bash +go version # Should show go1.25.0 or later +go env GOPATH # Should show a valid path +``` + +Once confirmed, proceed to [Installation](/docs/installation). diff --git a/frontend/content/docs/quick-start.mdx b/frontend/content/docs/quick-start.mdx new file mode 100644 index 00000000..28a28a8e --- /dev/null +++ b/frontend/content/docs/quick-start.mdx @@ -0,0 +1,151 @@ +--- +title: Quick Start +description: Build a complete cloud simulation in 5 minutes +--- + +# Quick Start + +Let's build a realistic cloud infrastructure simulation — VPC, compute instances, storage, DNS, and monitoring — all in memory. + +## Create an AWS Provider + +```go +package main + +import ( + "context" + "fmt" + + "github.com/stackshy/cloudemu" + computedriver "github.com/stackshy/cloudemu/compute/driver" + netdriver "github.com/stackshy/cloudemu/networking/driver" + storagedriver "github.com/stackshy/cloudemu/storage/driver" +) + +func main() { + ctx := context.Background() + aws := cloudemu.NewAWS() +``` + +## Set Up Networking + +Create a VPC, subnet, and security group: + +```go + // Create a VPC + vpc, _ := aws.VPC.CreateVPC(ctx, netdriver.VPCConfig{ + CIDRBlock: "10.0.0.0/16", + Tags: map[string]string{"env": "production"}, + }) + + // Add a subnet + subnet, _ := aws.VPC.CreateSubnet(ctx, netdriver.SubnetConfig{ + VPCID: vpc.ID, + CIDRBlock: "10.0.1.0/24", + AvailabilityZone: "us-east-1a", + }) + + // Create a security group with HTTPS access + sg, _ := aws.VPC.CreateSecurityGroup(ctx, netdriver.SecurityGroupConfig{ + Name: "web-sg", Description: "Web traffic", VPCID: vpc.ID, + }) + aws.VPC.AddIngressRule(ctx, sg.ID, netdriver.SecurityRule{ + Protocol: "tcp", FromPort: 443, ToPort: 443, CIDR: "0.0.0.0/0", + }) +``` + +## Launch Compute Instances + +```go + // Launch 3 instances — they start in "pending" and transition to "running" + instances, _ := aws.EC2.RunInstances(ctx, computedriver.InstanceConfig{ + ImageID: "ami-0abcdef1234", + InstanceType: "t3.large", + SubnetID: subnet.ID, + SecurityGroups: []string{sg.ID}, + Tags: map[string]string{"app": "web-server"}, + }, 3) + + for _, inst := range instances { + fmt.Printf("Instance %s: state=%s ip=%s\n", + inst.ID, inst.State, inst.PrivateIP) + } + // Instance i-00000001: state=running ip=10.0.0.1 + // Instance i-00000002: state=running ip=10.0.0.2 + // Instance i-00000003: state=running ip=10.0.0.3 +``` + +## Store Objects + +```go + // Create a bucket and upload files + aws.S3.CreateBucket(ctx, "app-deployments") + aws.S3.PutObject(ctx, "app-deployments", "v1.0/app.jar", + []byte("binary-data"), "application/java-archive", nil) + aws.S3.PutObject(ctx, "app-deployments", "v1.0/config.yaml", + []byte("db: rds-prod\nport: 8080"), "text/yaml", nil) + + // List objects by prefix + result, _ := aws.S3.ListObjects(ctx, "app-deployments", + storagedriver.ListOptions{Prefix: "v1.0/"}) + fmt.Printf("Objects in v1.0/: %d\n", len(result.Objects)) + + // Retrieve an object + obj, _ := aws.S3.GetObject(ctx, "app-deployments", "v1.0/config.yaml") + fmt.Printf("Config: %s\n", string(obj.Data)) +``` + +## Instance Lifecycle + +```go + // Stop an instance — state machine enforces valid transitions + aws.EC2.StopInstances(ctx, []string{instances[0].ID}) + + // Modify while stopped (resize) + aws.EC2.ModifyInstance(ctx, instances[0].ID, computedriver.ModifyInstanceInput{ + InstanceType: "t3.xlarge", + }) + + // Start it back + aws.EC2.StartInstances(ctx, []string{instances[0].ID}) + + // Terminate all + for _, inst := range instances { + aws.EC2.TerminateInstances(ctx, []string{inst.ID}) + } + + // Trying to stop a terminated instance returns an error + err := aws.EC2.StopInstances(ctx, []string{instances[0].ID}) + fmt.Println(err) // "cannot stop instance: invalid transition" +} +``` + +## Run It + +```bash +go run main.go +``` + +Everything runs in memory. No cloud account needed. No Docker. Zero cost. + +## Try Other Providers + +The same code works with Azure and GCP — just change the provider: + +```go +// Azure +azure := cloudemu.NewAzure() +azure.VirtualMachines.RunInstances(ctx, config, 3) +azure.BlobStorage.CreateBucket(ctx, "my-bucket") + +// GCP +gcp := cloudemu.NewGCP() +gcp.GCE.RunInstances(ctx, config, 3) +gcp.GCS.CreateBucket(ctx, "my-bucket") +``` + +## Next Steps + +- [Configuration](/docs/configuration) — customize regions, clocks, and more +- [Services](/docs/services) — explore all 16 service categories +- [Cross-Cutting Features](/docs/features) — add recording, metrics, and error injection diff --git a/frontend/content/docs/services/cache.mdx b/frontend/content/docs/services/cache.mdx new file mode 100644 index 00000000..e4d6767f --- /dev/null +++ b/frontend/content/docs/services/cache.mdx @@ -0,0 +1,44 @@ +--- +title: Cache +description: In-memory cache with TTL support +--- + +# Cache + +Emulates cache services: **ElastiCache** (AWS), **Cache** (Azure), **Memorystore** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | ElastiCache | `aws.ElastiCache` | +| Azure | Cache for Redis | `azure.Cache` | +| GCP | Memorystore | `gcp.Memorystore` | + +## Key Operations + +### Basic Operations + +```go +import cachedriver "github.com/stackshy/cloudemu/cache/driver" + +// Create a cache instance +aws.ElastiCache.CreateCacheInstance(ctx, cachedriver.CacheConfig{ + Name: "session-cache", + NodeType: "cache.t3.micro", +}) + +// Set a value with TTL +aws.ElastiCache.Set(ctx, "session-cache", "user:123", []byte("session-data"), 30*time.Minute) + +// Get a value +data, _ := aws.ElastiCache.Get(ctx, "session-cache", "user:123") + +// Delete +aws.ElastiCache.Delete(ctx, "session-cache", "user:123") +``` + +## Realistic Behaviors + +- **TTL expiry**: cached items are automatically expired after their TTL, with lazy cleanup on read +- **Thread-safe**: all cache operations are safe for concurrent access diff --git a/frontend/content/docs/services/compute.mdx b/frontend/content/docs/services/compute.mdx new file mode 100644 index 00000000..95e6759a --- /dev/null +++ b/frontend/content/docs/services/compute.mdx @@ -0,0 +1,104 @@ +--- +title: Compute +description: Virtual machine instances with lifecycle state machines +--- + +# Compute + +Emulates virtual machine services: **EC2** (AWS), **VirtualMachines** (Azure), **GCE** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | EC2 | `aws.EC2` | +| Azure | Virtual Machines | `azure.VirtualMachines` | +| GCP | GCE | `gcp.GCE` | + +## Key Operations + +### Instance Lifecycle + +```go +// Launch instances +instances, _ := aws.EC2.RunInstances(ctx, driver.InstanceConfig{ + ImageID: "ami-0abcdef1234", + InstanceType: "t3.large", + SubnetID: "subnet-123", + Tags: map[string]string{"env": "prod"}, +}, 3) + +// Stop, start, reboot +aws.EC2.StopInstances(ctx, []string{"i-00000001"}) +aws.EC2.StartInstances(ctx, []string{"i-00000001"}) +aws.EC2.RebootInstances(ctx, []string{"i-00000001"}) + +// Modify while stopped +aws.EC2.ModifyInstance(ctx, "i-00000001", driver.ModifyInstanceInput{ + InstanceType: "t3.xlarge", +}) + +// Terminate +aws.EC2.TerminateInstances(ctx, []string{"i-00000001"}) +``` + +### Describe and Filter + +```go +// Get specific instances +instances, _ := aws.EC2.DescribeInstances(ctx, []string{"i-00000001"}, nil) + +// Filter by state +running, _ := aws.EC2.DescribeInstances(ctx, nil, []driver.DescribeFilter{ + {Name: "instance-state-name", Values: []string{"running"}}, +}) +``` + +### Auto-Scaling Groups + +```go +asg, _ := aws.EC2.CreateAutoScalingGroup(ctx, driver.AutoScalingGroupConfig{ + Name: "web-asg", MinSize: 1, MaxSize: 10, DesiredCapacity: 3, + InstanceConfig: driver.InstanceConfig{ImageID: "ami-123", InstanceType: "t3.micro"}, +}) + +aws.EC2.SetDesiredCapacity(ctx, "web-asg", 5) +``` + +### Spot Instances + +```go +requests, _ := aws.EC2.RequestSpotInstances(ctx, driver.SpotRequestConfig{ + InstanceConfig: driver.InstanceConfig{ImageID: "ami-123", InstanceType: "t3.micro"}, + MaxPrice: 0.05, Count: 2, Type: "one-time", +}) +``` + +### Launch Templates + +```go +template, _ := aws.EC2.CreateLaunchTemplate(ctx, driver.LaunchTemplateConfig{ + Name: "web-template", + InstanceConfig: driver.InstanceConfig{ImageID: "ami-123", InstanceType: "t3.large"}, +}) +``` + +## State Machine + +Instances follow a strict state machine: + +``` +pending → running → stopping → stopped → starting → running + → shutting-down → terminated +``` + +Invalid transitions return `FailedPrecondition` errors. For example, you cannot stop a terminated instance. + +## Auto-Metrics + +When instances are launched, cloudemu automatically pushes metrics to the monitoring service: +- `CPUUtilization` — per instance +- `NetworkIn` / `NetworkOut` — per instance +- `DiskReadOps` / `DiskWriteOps` — per instance + +These metrics are queryable through `CloudWatch.GetMetricData()`. diff --git a/frontend/content/docs/services/containerregistry.mdx b/frontend/content/docs/services/containerregistry.mdx new file mode 100644 index 00000000..d3f2715f --- /dev/null +++ b/frontend/content/docs/services/containerregistry.mdx @@ -0,0 +1,51 @@ +--- +title: Container Registry +description: Container image storage, lifecycle policies, and scanning +--- + +# Container Registry + +Emulates container registries: **ECR** (AWS), **ACR** (Azure), **ArtifactRegistry** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | ECR | `aws.ECR` | +| Azure | ACR | `azure.ACR` | +| GCP | Artifact Registry | `gcp.ArtifactRegistry` | + +## Key Operations + +### Repositories + +```go +import crdriver "github.com/stackshy/cloudemu/containerregistry/driver" + +aws.ECR.CreateRepository(ctx, crdriver.RepositoryConfig{ + Name: "my-app", + Tags: map[string]string{"team": "platform"}, +}) + +repos, _ := aws.ECR.ListRepositories(ctx) +``` + +### Image Management + +```go +aws.ECR.PushImage(ctx, crdriver.PushImageInput{ + Repository: "my-app", + Tag: "v1.0.0", + Digest: "sha256:abc123...", +}) + +images, _ := aws.ECR.ListImages(ctx, "my-app") +``` + +### Lifecycle Policies + +Configure policies to automatically clean up old or untagged images. + +### Image Scanning + +Trigger vulnerability scans on pushed images. diff --git a/frontend/content/docs/services/database.mdx b/frontend/content/docs/services/database.mdx new file mode 100644 index 00000000..b6356655 --- /dev/null +++ b/frontend/content/docs/services/database.mdx @@ -0,0 +1,116 @@ +--- +title: Database +description: NoSQL database with queries, TTL, and change streams +--- + +# Database + +Emulates NoSQL databases: **DynamoDB** (AWS), **CosmosDB** (Azure), **Firestore** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | DynamoDB | `aws.DynamoDB` | +| Azure | CosmosDB | `azure.CosmosDB` | +| GCP | Firestore | `gcp.Firestore` | + +## Key Operations + +### Table Management + +```go +aws.DynamoDB.CreateTable(ctx, driver.TableConfig{ + Name: "users", + PartitionKey: "userId", + SortKey: "email", +}) + +tables, _ := aws.DynamoDB.ListTables(ctx) +``` + +### Item Operations + +```go +// Put +aws.DynamoDB.PutItem(ctx, "users", map[string]any{ + "userId": "u-001", "email": "alice@example.com", "name": "Alice", "age": 30, +}) + +// Get +item, _ := aws.DynamoDB.GetItem(ctx, "users", map[string]any{ + "userId": "u-001", "email": "alice@example.com", +}) + +// Delete +aws.DynamoDB.DeleteItem(ctx, "users", map[string]any{ + "userId": "u-001", "email": "alice@example.com", +}) + +// Batch operations +aws.DynamoDB.BatchPutItems(ctx, "users", []map[string]any{item1, item2, item3}) +items, _ := aws.DynamoDB.BatchGetItems(ctx, "users", []map[string]any{key1, key2}) +``` + +### Query and Scan + +```go +// Query by key condition +result, _ := aws.DynamoDB.Query(ctx, driver.QueryInput{ + Table: "users", + KeyCondition: driver.KeyCondition{ + PartitionKey: "userId", PartitionVal: "u-001", + SortOp: "BEGINS_WITH", SortVal: "a", + }, + Limit: 10, +}) + +// Scan with filters +result, _ = aws.DynamoDB.Scan(ctx, driver.ScanInput{ + Table: "users", + Filters: []driver.ScanFilter{ + {Field: "age", Op: ">", Value: 25}, + }, +}) +``` + +### TTL + +```go +aws.DynamoDB.UpdateTTL(ctx, "sessions", driver.TTLConfig{ + Enabled: true, + AttributeName: "expiresAt", +}) +``` + +Items with an `expiresAt` timestamp in the past are automatically cleaned up on read. + +### Streams / Change Feed + +```go +aws.DynamoDB.UpdateStreamConfig(ctx, "users", driver.StreamConfig{ + Enabled: true, + ViewType: "NEW_AND_OLD_IMAGES", +}) + +// After mutations, read the stream +iter, _ := aws.DynamoDB.GetStreamRecords(ctx, "users", 100, "") +for _, record := range iter.Records { + fmt.Println(record.EventType) // "INSERT", "MODIFY", or "REMOVE" +} +``` + +### Transactions + +```go +aws.DynamoDB.TransactWriteItems(ctx, "users", + []map[string]any{newItem1, newItem2}, // puts + []map[string]any{deleteKey1}, // deletes +) +``` + +## Realistic Behaviors + +- **Numeric-aware comparisons**: filters compare `"10" > "9"` correctly +- **TTL lazy cleanup**: expired items are removed on read, not in the background +- **Stream records**: every mutation (INSERT/MODIFY/REMOVE) produces a stream record with old and new images diff --git a/frontend/content/docs/services/dns.mdx b/frontend/content/docs/services/dns.mdx new file mode 100644 index 00000000..d1873557 --- /dev/null +++ b/frontend/content/docs/services/dns.mdx @@ -0,0 +1,48 @@ +--- +title: DNS +description: DNS zones and records with weighted routing +--- + +# DNS + +Emulates DNS services: **Route53** (AWS), **DNS** (Azure), **CloudDNS** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | Route53 | `aws.Route53` | +| Azure | DNS | `azure.DNS` | +| GCP | Cloud DNS | `gcp.CloudDNS` | + +## Key Operations + +### Zones + +```go +import dnsdriver "github.com/stackshy/cloudemu/dns/driver" + +zone, _ := aws.Route53.CreateZone(ctx, dnsdriver.ZoneConfig{ + Name: "example.com", +}) + +zones, _ := aws.Route53.ListZones(ctx) +``` + +### Records + +```go +aws.Route53.CreateRecord(ctx, dnsdriver.RecordConfig{ + ZoneID: zone.ID, + Name: "api.example.com", + Type: "A", + TTL: 300, + Values: []string{"10.0.0.1", "10.0.0.2"}, +}) + +records, _ := aws.Route53.ListRecords(ctx, zone.ID) +``` + +### Weighted Routing + +DNS records support weighted routing for load distribution across multiple endpoints. diff --git a/frontend/content/docs/services/eventbus.mdx b/frontend/content/docs/services/eventbus.mdx new file mode 100644 index 00000000..25ecdcfd --- /dev/null +++ b/frontend/content/docs/services/eventbus.mdx @@ -0,0 +1,56 @@ +--- +title: Event Bus +description: Event routing with rules and targets +--- + +# Event Bus + +Emulates event routing: **EventBridge** (AWS), **EventGrid** (Azure), **Eventarc** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | EventBridge | `aws.EventBridge` | +| Azure | Event Grid | `azure.EventGrid` | +| GCP | Eventarc | `gcp.Eventarc` | + +## Key Operations + +### Event Buses + +```go +import ebdriver "github.com/stackshy/cloudemu/eventbus/driver" + +bus, _ := aws.EventBridge.CreateEventBus(ctx, ebdriver.EventBusConfig{ + Name: "app-events", +}) +``` + +### Rules and Targets + +```go +aws.EventBridge.CreateRule(ctx, ebdriver.RuleConfig{ + EventBusID: bus.ID, + Name: "order-rule", + Pattern: `{"source": ["orders"]}`, +}) + +aws.EventBridge.AddTarget(ctx, ebdriver.TargetConfig{ + RuleID: "order-rule", + TargetID: "process-order", +}) +``` + +### Publishing Events + +```go +aws.EventBridge.PutEvents(ctx, []ebdriver.Event{ + { + Source: "orders", + DetailType: "OrderCreated", + Detail: `{"orderId": "123"}`, + EventBusID: bus.ID, + }, +}) +``` diff --git a/frontend/content/docs/services/iam.mdx b/frontend/content/docs/services/iam.mdx new file mode 100644 index 00000000..2c126420 --- /dev/null +++ b/frontend/content/docs/services/iam.mdx @@ -0,0 +1,71 @@ +--- +title: IAM +description: Identity, roles, and policy evaluation with wildcard matching +--- + +# IAM + +Emulates identity and access management across all three providers. + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | IAM | `aws.IAM` | +| Azure | IAM | `azure.IAM` | +| GCP | IAM | `gcp.IAM` | + +## Key Operations + +### Users and Roles + +```go +import iamdriver "github.com/stackshy/cloudemu/iam/driver" + +// Create a user +aws.IAM.CreateUser(ctx, iamdriver.UserConfig{ + Name: "alice", Tags: map[string]string{"team": "backend"}, +}) + +// Create a role +aws.IAM.CreateRole(ctx, iamdriver.RoleConfig{ + Name: "s3-reader", + AssumeRolePolicy: `{"Version":"2012-10-17","Statement":[...]}`, +}) +``` + +### Policies + +```go +// Attach a policy +aws.IAM.AttachPolicy(ctx, iamdriver.AttachPolicyInput{ + TargetType: "user", + TargetName: "alice", + PolicyDocument: `{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::my-bucket/*" + }] + }`, +}) +``` + +### Permission Checking + +```go +allowed, _ := aws.IAM.CheckPermission(ctx, iamdriver.PermissionCheck{ + Principal: "alice", + Action: "s3:GetObject", + Resource: "arn:aws:s3:::my-bucket/file.txt", +}) +// allowed == true +``` + +## Policy Evaluation + +cloudemu parses JSON policy documents with full support for: +- **Wildcard matching** in actions and resources (`s3:*`, `arn:aws:s3:::*`) +- **Explicit Deny overrides Allow** — matching real IAM behavior +- **Multiple statements** with different effects diff --git a/frontend/content/docs/services/index.mdx b/frontend/content/docs/services/index.mdx new file mode 100644 index 00000000..0d33dcc8 --- /dev/null +++ b/frontend/content/docs/services/index.mdx @@ -0,0 +1,55 @@ +--- +title: Services Overview +description: All 16 service categories across AWS, Azure, and GCP +--- + +# Services Overview + +cloudemu provides **16 service categories**, each implemented for all three cloud providers — **48 total implementations**. + +## Service Mapping + +| Category | AWS | Azure | GCP | +|----------|-----|-------|-----| +| [Compute](/docs/services/compute) | EC2 | VirtualMachines | GCE | +| [Storage](/docs/services/storage) | S3 | BlobStorage | GCS | +| [Database](/docs/services/database) | DynamoDB | CosmosDB | Firestore | +| [Serverless](/docs/services/serverless) | Lambda | Functions | CloudFunctions | +| [Networking](/docs/services/networking) | VPC | VNet | VPC | +| [Monitoring](/docs/services/monitoring) | CloudWatch | Monitor | CloudMonitoring | +| [IAM](/docs/services/iam) | IAM | IAM | IAM | +| [DNS](/docs/services/dns) | Route53 | DNS | CloudDNS | +| [Load Balancer](/docs/services/loadbalancer) | ELB | LB | LB | +| [Message Queue](/docs/services/messagequeue) | SQS | ServiceBus | PubSub | +| [Notification](/docs/services/notification) | SNS | NotificationHubs | FCM | +| [Event Bus](/docs/services/eventbus) | EventBridge | EventGrid | Eventarc | +| [Container Registry](/docs/services/containerregistry) | ECR | ACR | ArtifactRegistry | +| [Cache](/docs/services/cache) | ElastiCache | Cache | Memorystore | +| [Secrets](/docs/services/secrets) | SecretsManager | KeyVault | SecretManager | +| [Logging](/docs/services/logging) | CloudWatchLogs | LogAnalytics | CloudLogging | + +## Accessing Services + +Each provider exposes services as typed fields: + +```go +aws := cloudemu.NewAWS() +aws.EC2 // Compute +aws.S3 // Storage +aws.DynamoDB // Database +aws.Lambda // Serverless +aws.VPC // Networking +aws.CloudWatch // Monitoring + +azure := cloudemu.NewAzure() +azure.VirtualMachines // Compute +azure.BlobStorage // Storage +azure.CosmosDB // Database + +gcp := cloudemu.NewGCP() +gcp.GCE // Compute +gcp.GCS // Storage +gcp.Firestore // Database +``` + +All providers implement the same driver interfaces, so the operations and behaviors are consistent across clouds. diff --git a/frontend/content/docs/services/loadbalancer.mdx b/frontend/content/docs/services/loadbalancer.mdx new file mode 100644 index 00000000..cfea1f62 --- /dev/null +++ b/frontend/content/docs/services/loadbalancer.mdx @@ -0,0 +1,57 @@ +--- +title: Load Balancer +description: Load balancers, target groups, listeners, and health checks +--- + +# Load Balancer + +Emulates load balancing: **ELB** (AWS), **LB** (Azure), **LB** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | ELB | `aws.ELB` | +| Azure | LB | `azure.LB` | +| GCP | LB | `gcp.LB` | + +## Key Operations + +### Load Balancers + +```go +import lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver" + +lb, _ := aws.ELB.CreateLoadBalancer(ctx, lbdriver.LoadBalancerConfig{ + Name: "web-lb", + Type: "application", + Scheme: "internet-facing", +}) +``` + +### Target Groups + +```go +tg, _ := aws.ELB.CreateTargetGroup(ctx, lbdriver.TargetGroupConfig{ + Name: "web-targets", + Port: 8080, + Protocol: "HTTP", +}) + +aws.ELB.RegisterTargets(ctx, tg.ID, []string{"i-00000001", "i-00000002"}) +``` + +### Listeners + +```go +aws.ELB.CreateListener(ctx, lbdriver.ListenerConfig{ + LoadBalancerID: lb.ID, + Port: 443, + Protocol: "HTTPS", + TargetGroupID: tg.ID, +}) +``` + +### Health Checks + +Target groups include health check configuration that determines target health status. diff --git a/frontend/content/docs/services/logging.mdx b/frontend/content/docs/services/logging.mdx new file mode 100644 index 00000000..ccb04a4b --- /dev/null +++ b/frontend/content/docs/services/logging.mdx @@ -0,0 +1,49 @@ +--- +title: Logging +description: Log groups and log streams +--- + +# Logging + +Emulates logging services: **CloudWatch Logs** (AWS), **Log Analytics** (Azure), **Cloud Logging** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | CloudWatch Logs | `aws.CloudWatchLogs` | +| Azure | Log Analytics | `azure.LogAnalytics` | +| GCP | Cloud Logging | `gcp.CloudLogging` | + +## Key Operations + +### Log Groups and Streams + +```go +import logdriver "github.com/stackshy/cloudemu/logging/driver" + +// Create a log group +aws.CloudWatchLogs.CreateLogGroup(ctx, logdriver.LogGroupConfig{ + Name: "/app/web", + RetentionDays: 30, +}) + +// Create a log stream +aws.CloudWatchLogs.CreateLogStream(ctx, "/app/web", "instance-001") + +// Put log events +aws.CloudWatchLogs.PutLogEvents(ctx, "/app/web", "instance-001", []logdriver.LogEvent{ + {Timestamp: time.Now(), Message: "Server started on port 8080"}, + {Timestamp: time.Now(), Message: "Handling request GET /api/users"}, +}) +``` + +### Querying Logs + +```go +events, _ := aws.CloudWatchLogs.GetLogEvents(ctx, "/app/web", "instance-001", + logdriver.GetLogEventsInput{ + StartTime: time.Now().Add(-1 * time.Hour), + Limit: 100, + }) +``` diff --git a/frontend/content/docs/services/messagequeue.mdx b/frontend/content/docs/services/messagequeue.mdx new file mode 100644 index 00000000..fe98c903 --- /dev/null +++ b/frontend/content/docs/services/messagequeue.mdx @@ -0,0 +1,74 @@ +--- +title: Message Queue +description: Queues with FIFO deduplication and dead-letter queues +--- + +# Message Queue + +Emulates message queues: **SQS** (AWS), **ServiceBus** (Azure), **PubSub** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | SQS | `aws.SQS` | +| Azure | Service Bus | `azure.ServiceBus` | +| GCP | Pub/Sub | `gcp.PubSub` | + +## Key Operations + +### Queue Management + +```go +import mqdriver "github.com/stackshy/cloudemu/messagequeue/driver" + +aws.SQS.CreateQueue(ctx, mqdriver.QueueConfig{ + Name: "orders", + Attributes: map[string]string{ + "VisibilityTimeout": "30", + }, +}) + +// FIFO queue +aws.SQS.CreateQueue(ctx, mqdriver.QueueConfig{ + Name: "orders.fifo", + FIFOQueue: true, +}) +``` + +### Send and Receive Messages + +```go +// Send +aws.SQS.SendMessage(ctx, "orders", mqdriver.SendMessageInput{ + Body: "order-123", + Attributes: map[string]string{"priority": "high"}, +}) + +// Receive +messages, _ := aws.SQS.ReceiveMessages(ctx, "orders", mqdriver.ReceiveInput{ + MaxMessages: 10, + WaitTimeSeconds: 0, +}) + +// Delete after processing +aws.SQS.DeleteMessage(ctx, "orders", messages[0].ReceiptHandle) +``` + +### Batch Operations + +```go +aws.SQS.SendMessageBatch(ctx, "orders", []mqdriver.SendMessageInput{ + {Body: "order-1"}, {Body: "order-2"}, {Body: "order-3"}, +}) +``` + +### Dead-Letter Queues + +Configure a DLQ and messages exceeding the max receive count automatically move there. + +## Realistic Behaviors + +- **FIFO deduplication**: FIFO queues enforce 5-minute deduplication windows using `MessageDeduplicationId` +- **Visibility timeout**: received messages are invisible to other consumers until timeout expires or message is deleted +- **Dead-letter queues**: messages exceeding max receive count are automatically moved to the DLQ diff --git a/frontend/content/docs/services/meta.json b/frontend/content/docs/services/meta.json new file mode 100644 index 00000000..6f5b3a1b --- /dev/null +++ b/frontend/content/docs/services/meta.json @@ -0,0 +1,22 @@ +{ + "title": "Services", + "pages": [ + "index", + "compute", + "storage", + "database", + "serverless", + "networking", + "monitoring", + "iam", + "dns", + "loadbalancer", + "messagequeue", + "notification", + "eventbus", + "containerregistry", + "cache", + "secrets", + "logging" + ] +} diff --git a/frontend/content/docs/services/monitoring.mdx b/frontend/content/docs/services/monitoring.mdx new file mode 100644 index 00000000..0d20872c --- /dev/null +++ b/frontend/content/docs/services/monitoring.mdx @@ -0,0 +1,90 @@ +--- +title: Monitoring +description: Metrics, alarms, and metric queries +--- + +# Monitoring + +Emulates monitoring services: **CloudWatch** (AWS), **Monitor** (Azure), **CloudMonitoring** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | CloudWatch | `aws.CloudWatch` | +| Azure | Monitor | `azure.Monitor` | +| GCP | Cloud Monitoring | `gcp.CloudMonitoring` | + +## Key Operations + +### Push Metrics + +```go +import mondriver "github.com/stackshy/cloudemu/monitoring/driver" + +aws.CloudWatch.PutMetricData(ctx, []mondriver.MetricDatum{ + { + Namespace: "App/Web", + MetricName: "CPUUtilization", + Value: 72.8, + Timestamp: time.Now(), + Dimensions: map[string]string{"InstanceId": "i-00000001"}, + }, + { + Namespace: "App/Web", + MetricName: "RequestCount", + Value: 15230, + Timestamp: time.Now(), + }, +}) +``` + +### Query Metrics + +```go +result, _ := aws.CloudWatch.GetMetricData(ctx, mondriver.GetMetricInput{ + Namespace: "App/Web", + MetricName: "CPUUtilization", + Dimensions: map[string]string{"InstanceId": "i-00000001"}, + StartTime: time.Now().Add(-5 * time.Minute), + EndTime: time.Now(), + Period: 60, + Stat: "Average", // also: "Sum", "Minimum", "Maximum", "SampleCount" +}) + +fmt.Printf("CPU: %.1f%%\n", result.Values[0]) +``` + +### List Metrics + +```go +metrics, _ := aws.CloudWatch.ListMetrics(ctx, "App/Web") +// ["CPUUtilization", "RequestCount"] +``` + +### Alarms + +```go +// Create an alarm +aws.CloudWatch.CreateAlarm(ctx, mondriver.AlarmConfig{ + Name: "high-cpu", + Namespace: "App/Web", + MetricName: "CPUUtilization", + ComparisonOperator: "GreaterThanThreshold", + Threshold: 80, + Period: 300, + EvaluationPeriods: 2, + Stat: "Average", +}) + +// List alarms +alarms, _ := aws.CloudWatch.DescribeAlarms(ctx, nil) +``` + +## Auto-Metrics + +Services like EC2 automatically push metrics to the monitoring service when instances are launched, stopped, or terminated. You can query these exactly as you would in production. + +## Alarm Evaluation + +Alarms automatically evaluate when new metric data is pushed. They transition between `OK` and `ALARM` states based on threshold comparison. diff --git a/frontend/content/docs/services/networking.mdx b/frontend/content/docs/services/networking.mdx new file mode 100644 index 00000000..50187f73 --- /dev/null +++ b/frontend/content/docs/services/networking.mdx @@ -0,0 +1,91 @@ +--- +title: Networking +description: Virtual networks, subnets, security groups, and peering +--- + +# Networking + +Emulates virtual networking: **VPC** (AWS), **VNet** (Azure), **VPC** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | VPC | `aws.VPC` | +| Azure | VNet | `azure.VNet` | +| GCP | VPC | `gcp.VPC` | + +## Key Operations + +### VPCs / Virtual Networks + +```go +import netdriver "github.com/stackshy/cloudemu/networking/driver" + +vpc, _ := aws.VPC.CreateVPC(ctx, netdriver.VPCConfig{ + CIDRBlock: "10.0.0.0/16", + Tags: map[string]string{"env": "production"}, +}) +``` + +### Subnets + +```go +subnet, _ := aws.VPC.CreateSubnet(ctx, netdriver.SubnetConfig{ + VPCID: vpc.ID, + CIDRBlock: "10.0.1.0/24", + AvailabilityZone: "us-east-1a", +}) +``` + +### Security Groups + +```go +sg, _ := aws.VPC.CreateSecurityGroup(ctx, netdriver.SecurityGroupConfig{ + Name: "web-sg", Description: "Web traffic", VPCID: vpc.ID, +}) + +// Add ingress rule +aws.VPC.AddIngressRule(ctx, sg.ID, netdriver.SecurityRule{ + Protocol: "tcp", FromPort: 443, ToPort: 443, CIDR: "0.0.0.0/0", +}) + +// Add egress rule +aws.VPC.AddEgressRule(ctx, sg.ID, netdriver.SecurityRule{ + Protocol: "tcp", FromPort: 0, ToPort: 65535, CIDR: "0.0.0.0/0", +}) +``` + +### VPC Peering + +```go +peering, _ := aws.VPC.CreatePeeringConnection(ctx, netdriver.PeeringConfig{ + RequesterVPCID: vpc1.ID, AccepterVPCID: vpc2.ID, +}) +aws.VPC.AcceptPeeringConnection(ctx, peering.ID) +``` + +### NAT Gateways + +```go +nat, _ := aws.VPC.CreateNATGateway(ctx, netdriver.NATGatewayConfig{ + SubnetID: subnet.ID, +}) +``` + +### Route Tables + +```go +rt, _ := aws.VPC.CreateRouteTable(ctx, netdriver.RouteTableConfig{ + VPCID: vpc.ID, +}) +aws.VPC.AssociateRouteTable(ctx, rt.ID, subnet.ID) +``` + +### Flow Logs + +```go +aws.VPC.CreateFlowLog(ctx, netdriver.FlowLogConfig{ + ResourceID: vpc.ID, TrafficType: "ALL", +}) +``` diff --git a/frontend/content/docs/services/notification.mdx b/frontend/content/docs/services/notification.mdx new file mode 100644 index 00000000..e30a6a64 --- /dev/null +++ b/frontend/content/docs/services/notification.mdx @@ -0,0 +1,44 @@ +--- +title: Notification +description: Topics, subscriptions, and push notifications +--- + +# Notification + +Emulates notification services: **SNS** (AWS), **NotificationHubs** (Azure), **FCM** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | SNS | `aws.SNS` | +| Azure | Notification Hubs | `azure.NotificationHubs` | +| GCP | FCM | `gcp.FCM` | + +## Key Operations + +### Topics and Subscriptions + +```go +import notifdriver "github.com/stackshy/cloudemu/notification/driver" + +topic, _ := aws.SNS.CreateTopic(ctx, notifdriver.TopicConfig{ + Name: "order-events", +}) + +sub, _ := aws.SNS.Subscribe(ctx, notifdriver.SubscriptionConfig{ + TopicID: topic.ID, + Protocol: "email", + Endpoint: "team@example.com", +}) +``` + +### Publishing + +```go +aws.SNS.Publish(ctx, notifdriver.PublishInput{ + TopicID: topic.ID, + Message: "New order received", + Attributes: map[string]string{"orderType": "express"}, +}) +``` diff --git a/frontend/content/docs/services/secrets.mdx b/frontend/content/docs/services/secrets.mdx new file mode 100644 index 00000000..c786cd96 --- /dev/null +++ b/frontend/content/docs/services/secrets.mdx @@ -0,0 +1,53 @@ +--- +title: Secrets +description: Secret storage and versioning +--- + +# Secrets + +Emulates secret management: **SecretsManager** (AWS), **KeyVault** (Azure), **SecretManager** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | Secrets Manager | `aws.SecretsManager` | +| Azure | Key Vault | `azure.KeyVault` | +| GCP | Secret Manager | `gcp.SecretManager` | + +## Key Operations + +### Creating Secrets + +```go +import secdriver "github.com/stackshy/cloudemu/secrets/driver" + +aws.SecretsManager.CreateSecret(ctx, secdriver.SecretConfig{ + Name: "db-password", + Value: "super-secret-123", + Tags: map[string]string{"env": "production"}, +}) +``` + +### Reading Secrets + +```go +secret, _ := aws.SecretsManager.GetSecret(ctx, "db-password") +fmt.Println(secret.Value) // "super-secret-123" +``` + +### Versioning + +```go +// Update creates a new version +aws.SecretsManager.UpdateSecret(ctx, "db-password", "new-password-456") + +// Get specific version +secret, _ = aws.SecretsManager.GetSecretVersion(ctx, "db-password", "v2") +``` + +### Listing + +```go +secrets, _ := aws.SecretsManager.ListSecrets(ctx) +``` diff --git a/frontend/content/docs/services/serverless.mdx b/frontend/content/docs/services/serverless.mdx new file mode 100644 index 00000000..9673de4a --- /dev/null +++ b/frontend/content/docs/services/serverless.mdx @@ -0,0 +1,75 @@ +--- +title: Serverless +description: Function-as-a-service with versions, aliases, and layers +--- + +# Serverless + +Emulates serverless functions: **Lambda** (AWS), **Functions** (Azure), **CloudFunctions** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | Lambda | `aws.Lambda` | +| Azure | Functions | `azure.Functions` | +| GCP | Cloud Functions | `gcp.CloudFunctions` | + +## Key Operations + +### Function Lifecycle + +```go +import sdriver "github.com/stackshy/cloudemu/serverless/driver" + +// Create a function +aws.Lambda.CreateFunction(ctx, sdriver.FunctionConfig{ + Name: "my-handler", + Runtime: "go1.x", + Handler: "main", + Memory: 128, + Timeout: 30, + Tags: map[string]string{"team": "backend"}, +}) + +// Register a Go handler +aws.Lambda.RegisterHandler(ctx, "my-handler", func(ctx context.Context, payload []byte) ([]byte, error) { + return []byte(`{"status": "ok"}`), nil +}) + +// Invoke +result, _ := aws.Lambda.InvokeFunction(ctx, "my-handler", []byte(`{"key": "value"}`)) +``` + +### Versions and Aliases + +```go +// Publish a version +version, _ := aws.Lambda.PublishVersion(ctx, "my-handler", "v1 release") + +// Create an alias pointing to a version +aws.Lambda.CreateAlias(ctx, sdriver.AliasConfig{ + FunctionName: "my-handler", Name: "prod", FunctionVersion: "1", +}) + +// Weighted routing between versions +aws.Lambda.UpdateAlias(ctx, "my-handler", "prod", sdriver.AliasUpdate{ + FunctionVersion: "2", + AdditionalVersionWeights: map[string]float64{"1": 0.1}, // 10% to v1 +}) +``` + +### Layers + +```go +layer, _ := aws.Lambda.PublishLayerVersion(ctx, sdriver.LayerConfig{ + Name: "common-libs", Description: "Shared libraries", +}) +``` + +### Concurrency + +```go +aws.Lambda.PutFunctionConcurrency(ctx, "my-handler", 100) // reserve 100 concurrent executions +conc, _ := aws.Lambda.GetFunctionConcurrency(ctx, "my-handler") +``` diff --git a/frontend/content/docs/services/storage.mdx b/frontend/content/docs/services/storage.mdx new file mode 100644 index 00000000..753be978 --- /dev/null +++ b/frontend/content/docs/services/storage.mdx @@ -0,0 +1,108 @@ +--- +title: Storage +description: Object storage with buckets, versioning, and multipart upload +--- + +# Storage + +Emulates object storage: **S3** (AWS), **BlobStorage** (Azure), **GCS** (GCP). + +## Provider Mapping + +| Provider | Service | Access | +|----------|---------|--------| +| AWS | S3 | `aws.S3` | +| Azure | Blob Storage | `azure.BlobStorage` | +| GCP | GCS | `gcp.GCS` | + +## Key Operations + +### Bucket Management + +```go +aws.S3.CreateBucket(ctx, "my-bucket") + +buckets, _ := aws.S3.ListBuckets(ctx) + +aws.S3.DeleteBucket(ctx, "my-bucket") +``` + +### Object Operations + +```go +// Upload +aws.S3.PutObject(ctx, "my-bucket", "path/to/file.txt", + []byte("content"), "text/plain", map[string]string{"author": "me"}) + +// Download +obj, _ := aws.S3.GetObject(ctx, "my-bucket", "path/to/file.txt") +fmt.Println(string(obj.Data)) // "content" + +// Head (metadata only) +info, _ := aws.S3.HeadObject(ctx, "my-bucket", "path/to/file.txt") +fmt.Println(info.ContentType) // "text/plain" + +// Delete +aws.S3.DeleteObject(ctx, "my-bucket", "path/to/file.txt") + +// Copy +aws.S3.CopyObject(ctx, "dst-bucket", "copy.txt", driver.CopySource{ + Bucket: "my-bucket", Key: "path/to/file.txt", +}) +``` + +### List with Prefix and Delimiter + +```go +// List all objects with a prefix +result, _ := aws.S3.ListObjects(ctx, "my-bucket", driver.ListOptions{ + Prefix: "v1.0/", +}) + +// Folder-like listing with delimiter +result, _ = aws.S3.ListObjects(ctx, "my-bucket", driver.ListOptions{ + Delimiter: "/", +}) +// result.CommonPrefixes contains folder-like prefixes +``` + +### Multipart Upload + +```go +upload, _ := aws.S3.CreateMultipartUpload(ctx, "bucket", "large-file.bin", "application/octet-stream") + +part1, _ := aws.S3.UploadPart(ctx, "bucket", "large-file.bin", upload.UploadID, 1, data1) +part2, _ := aws.S3.UploadPart(ctx, "bucket", "large-file.bin", upload.UploadID, 2, data2) + +aws.S3.CompleteMultipartUpload(ctx, "bucket", "large-file.bin", upload.UploadID, + []driver.UploadPart{*part1, *part2}) +``` + +### Versioning + +```go +aws.S3.SetBucketVersioning(ctx, "my-bucket", true) + +enabled, _ := aws.S3.GetBucketVersioning(ctx, "my-bucket") +``` + +### Presigned URLs + +```go +url, _ := aws.S3.GeneratePresignedURL(ctx, driver.PresignedURLRequest{ + Bucket: "my-bucket", Key: "file.txt", Method: "GET", + Expiry: 15 * time.Minute, +}) +``` + +### Lifecycle Policies + +```go +aws.S3.PutLifecycleConfig(ctx, "my-bucket", driver.LifecycleConfig{ + Rules: []driver.LifecycleRule{ + {Prefix: "logs/", ExpirationDays: 30, Enabled: true}, + }, +}) + +expired, _ := aws.S3.EvaluateLifecycle(ctx, "my-bucket") +``` diff --git a/frontend/lib/services.ts b/frontend/lib/services.ts new file mode 100644 index 00000000..97106e1b --- /dev/null +++ b/frontend/lib/services.ts @@ -0,0 +1,28 @@ +export interface ServiceMapping { + category: string; + icon: string; + aws: string; + azure: string; + gcp: string; + slug: string; + description: string; +} + +export const services: ServiceMapping[] = [ + { category: 'Compute', icon: 'Server', aws: 'EC2', azure: 'VirtualMachines', gcp: 'GCE', slug: 'compute', description: 'Virtual machine instances with lifecycle state machines' }, + { category: 'Storage', icon: 'HardDrive', aws: 'S3', azure: 'BlobStorage', gcp: 'GCS', slug: 'storage', description: 'Object storage with buckets, versioning, and multipart upload' }, + { category: 'Database', icon: 'Database', aws: 'DynamoDB', azure: 'CosmosDB', gcp: 'Firestore', slug: 'database', description: 'NoSQL database with queries, TTL, and streams' }, + { category: 'Serverless', icon: 'Zap', aws: 'Lambda', azure: 'Functions', gcp: 'CloudFunctions', slug: 'serverless', description: 'Function-as-a-service with versions and aliases' }, + { category: 'Networking', icon: 'Network', aws: 'VPC', azure: 'VNet', gcp: 'VPC', slug: 'networking', description: 'Virtual networks, subnets, and security groups' }, + { category: 'Monitoring', icon: 'Activity', aws: 'CloudWatch', azure: 'Monitor', gcp: 'CloudMonitoring', slug: 'monitoring', description: 'Metrics, alarms, and metric queries' }, + { category: 'IAM', icon: 'Shield', aws: 'IAM', azure: 'IAM', gcp: 'IAM', slug: 'iam', description: 'Identity, roles, and policy evaluation' }, + { category: 'DNS', icon: 'Globe', aws: 'Route53', azure: 'DNS', gcp: 'CloudDNS', slug: 'dns', description: 'DNS zones and records with weighted routing' }, + { category: 'Load Balancer', icon: 'GitBranch', aws: 'ELB', azure: 'LB', gcp: 'LB', slug: 'loadbalancer', description: 'Load balancers, target groups, and health checks' }, + { category: 'Message Queue', icon: 'MessageSquare', aws: 'SQS', azure: 'ServiceBus', gcp: 'PubSub', slug: 'messagequeue', description: 'Queues with FIFO dedup and dead-letter queues' }, + { category: 'Notification', icon: 'Bell', aws: 'SNS', azure: 'NotificationHubs', gcp: 'FCM', slug: 'notification', description: 'Topics, subscriptions, and push notifications' }, + { category: 'Event Bus', icon: 'Radio', aws: 'EventBridge', azure: 'EventGrid', gcp: 'Eventarc', slug: 'eventbus', description: 'Event routing with rules and targets' }, + { category: 'Container Registry', icon: 'Box', aws: 'ECR', azure: 'ACR', gcp: 'ArtifactRegistry', slug: 'containerregistry', description: 'Container image storage and lifecycle' }, + { category: 'Cache', icon: 'MemoryStick', aws: 'ElastiCache', azure: 'Cache', gcp: 'Memorystore', slug: 'cache', description: 'In-memory cache with TTL support' }, + { category: 'Secrets', icon: 'Lock', aws: 'SecretsManager', azure: 'KeyVault', gcp: 'SecretManager', slug: 'secrets', description: 'Secret storage and versioning' }, + { category: 'Logging', icon: 'FileText', aws: 'CloudWatchLogs', azure: 'LogAnalytics', gcp: 'CloudLogging', slug: 'logging', description: 'Log groups and log streams' }, +]; diff --git a/frontend/lib/source.ts b/frontend/lib/source.ts new file mode 100644 index 00000000..e935d703 --- /dev/null +++ b/frontend/lib/source.ts @@ -0,0 +1,8 @@ +import { docs, meta } from '@/.source/server'; +import { loader } from 'fumadocs-core/source'; +import { toFumadocsSource } from 'fumadocs-mdx/runtime/server'; + +export const source = loader({ + baseUrl: '/docs', + source: toFumadocsSource(docs, meta) as any, +}); diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs new file mode 100644 index 00000000..457dcf29 --- /dev/null +++ b/frontend/next.config.mjs @@ -0,0 +1,10 @@ +import { createMDX } from 'fumadocs-mdx/next'; + +const withMDX = createMDX(); + +/** @type {import('next').NextConfig} */ +const config = { + reactStrictMode: true, +}; + +export default withMDX(config); diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..122365ed --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,6248 @@ +{ + "name": "frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@tailwindcss/postcss": "^4.2.2", + "autoprefixer": "^10.4.27", + "framer-motion": "^12.38.0", + "fumadocs-core": "^16.7.6", + "fumadocs-mdx": "^14.2.11", + "fumadocs-ui": "^16.7.6", + "lucide-react": "^1.7.0", + "next": "^16.2.1", + "next-themes": "^0.4.6", + "postcss": "^8.5.8", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "tailwindcss": "^4.2.2" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "typescript": "^6.0.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.1.tgz", + "integrity": "sha512-CbNbf+tlJn1baRnPkNePnBqTLxGliG6DDgNa/UtV66abwIjwsliPMOt0172tzxABYzSuxZBZfcp//qI8AvBWPg==", + "license": "MIT" + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.2.tgz", + "integrity": "sha512-q05KMYGJLyqFNFtIb8NhWLF5X3aK/k0wYt7dnRFuy6aLQL+vUwQ1cg5cO4qawEiINybeCPXAWlprY2mSBjSXAQ==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.1" + } + }, + "node_modules/@fumadocs/tailwind": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@fumadocs/tailwind/-/tailwind-0.0.3.tgz", + "integrity": "sha512-/FWcggMz9BhoX+13xBoZLX+XX9mYvJ50dkTqy3IfocJqua65ExcsKfxwKH8hgTO3vA5KnWv4+4jU7LaW2AjAmQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.1.1" + }, + "peerDependencies": { + "tailwindcss": "^4.0.0" + }, + "peerDependenciesMeta": { + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@next/env": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz", + "integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz", + "integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", + "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", + "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", + "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", + "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", + "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", + "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", + "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@orama/orama": { + "version": "3.1.18", + "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-3.1.18.tgz", + "integrity": "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20.0.0" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@shikijs/core": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", + "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", + "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", + "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", + "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", + "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/rehype": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/rehype/-/rehype-4.0.2.tgz", + "integrity": "sha512-cmPlKLD8JeojasNFoY64162ScpEdEdQUMuVodPCrv1nx1z3bjmGwoKWDruQWa/ejSznImlaeB0Ty6Q3zPaVQAA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-string": "^3.0.1", + "shiki": "4.0.2", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", + "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/transformers": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-4.0.2.tgz", + "integrity": "sha512-1+L0gf9v+SdDXs08vjaLb3mBFa8U7u37cwcBQIv/HCocLwX69Tt6LpUCjtB+UUTvQxI7BnjZKhN/wMjhHBcJGg==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.0.2", + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", + "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "postcss": "^8.5.6", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.325", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz", + "integrity": "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==", + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-value-to-estree": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", + "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fumadocs-core": { + "version": "16.7.6", + "resolved": "https://registry.npmjs.org/fumadocs-core/-/fumadocs-core-16.7.6.tgz", + "integrity": "sha512-d4HtGupFpcSWQqLbWh184yoEg6D70pH68NP77Ct4mI0N61t/Uy63wYj9sbS1h/m6jlijUIXC6rz8D5JApOB9Wg==", + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.8.2", + "@orama/orama": "^3.1.18", + "@shikijs/rehype": "^4.0.2", + "@shikijs/transformers": "^4.0.2", + "estree-util-value-to-estree": "^3.5.0", + "github-slugger": "^2.0.0", + "hast-util-to-estree": "^3.1.3", + "hast-util-to-jsx-runtime": "^2.3.6", + "image-size": "^2.0.2", + "mdast-util-mdx": "^3.0.0", + "mdast-util-to-markdown": "^2.1.2", + "negotiator": "^1.0.0", + "npm-to-yarn": "^3.0.1", + "path-to-regexp": "^8.3.0", + "remark": "^15.0.1", + "remark-gfm": "^4.0.1", + "remark-rehype": "^11.1.2", + "scroll-into-view-if-needed": "^3.1.0", + "shiki": "^4.0.2", + "tinyglobby": "^0.2.15", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "peerDependencies": { + "@mdx-js/mdx": "*", + "@mixedbread/sdk": "^0.46.0", + "@orama/core": "1.x.x", + "@oramacloud/client": "2.x.x", + "@tanstack/react-router": "1.x.x", + "@types/estree-jsx": "*", + "@types/hast": "*", + "@types/mdast": "*", + "@types/react": "*", + "algoliasearch": "5.x.x", + "flexsearch": "*", + "lucide-react": "*", + "next": "16.x.x", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router": "7.x.x", + "waku": "^0.26.0 || ^0.27.0 || ^1.0.0", + "zod": "4.x.x" + }, + "peerDependenciesMeta": { + "@mdx-js/mdx": { + "optional": true + }, + "@mixedbread/sdk": { + "optional": true + }, + "@orama/core": { + "optional": true + }, + "@oramacloud/client": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "@types/estree-jsx": { + "optional": true + }, + "@types/hast": { + "optional": true + }, + "@types/mdast": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "algoliasearch": { + "optional": true + }, + "flexsearch": { + "optional": true + }, + "lucide-react": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-router": { + "optional": true + }, + "waku": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/fumadocs-mdx": { + "version": "14.2.11", + "resolved": "https://registry.npmjs.org/fumadocs-mdx/-/fumadocs-mdx-14.2.11.tgz", + "integrity": "sha512-j0gHKs45c62ARteE8/yBM2Nu2I8AE2Cs37ktPEdc/8EX7TL66XP74un5OpHp6itLyWTu8Jur0imOiiIDq8+rDg==", + "license": "MIT", + "dependencies": { + "@mdx-js/mdx": "^3.1.1", + "@standard-schema/spec": "^1.1.0", + "chokidar": "^5.0.0", + "esbuild": "^0.27.3", + "estree-util-value-to-estree": "^3.5.0", + "js-yaml": "^4.1.1", + "mdast-util-mdx": "^3.0.0", + "mdast-util-to-markdown": "^2.1.2", + "picocolors": "^1.1.1", + "picomatch": "^4.0.3", + "tinyexec": "^1.0.4", + "tinyglobby": "^0.2.15", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3", + "zod": "^4.3.6" + }, + "bin": { + "fumadocs-mdx": "dist/bin.js" + }, + "peerDependencies": { + "@fumadocs/mdx-remote": "^1.4.0", + "@types/mdast": "*", + "@types/mdx": "*", + "@types/react": "*", + "fumadocs-core": "^15.0.0 || ^16.0.0", + "mdast-util-directive": "*", + "next": "^15.3.0 || ^16.0.0", + "react": "*", + "vite": "6.x.x || 7.x.x || 8.x.x" + }, + "peerDependenciesMeta": { + "@fumadocs/mdx-remote": { + "optional": true + }, + "@types/mdast": { + "optional": true + }, + "@types/mdx": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "mdast-util-directive": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/fumadocs-ui": { + "version": "16.7.6", + "resolved": "https://registry.npmjs.org/fumadocs-ui/-/fumadocs-ui-16.7.6.tgz", + "integrity": "sha512-wjZnm8SiX2lj5zWOlOHnzSZ0YBFwNqYGBX1u5F3mZtdIkmkDVs+3+JngCkRHNZzYJVBulXjp8t5wzBz0yDJa8w==", + "license": "MIT", + "dependencies": { + "@fumadocs/tailwind": "0.0.3", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-direction": "^1.1.1", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-presence": "^1.1.5", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "class-variance-authority": "^0.7.1", + "lucide-react": "^1.6.0", + "motion": "^12.38.0", + "next-themes": "^0.4.6", + "react-medium-image-zoom": "^5.4.1", + "react-remove-scroll": "^2.7.2", + "rehype-raw": "^7.0.0", + "scroll-into-view-if-needed": "^3.1.0", + "tailwind-merge": "^3.5.0", + "unist-util-visit": "^5.1.0" + }, + "peerDependencies": { + "@takumi-rs/image-response": "*", + "@types/mdx": "*", + "@types/react": "*", + "fumadocs-core": "16.7.6", + "next": "16.x.x", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "shiki": "*" + }, + "peerDependenciesMeta": { + "@takumi-rs/image-response": { + "optional": true + }, + "@types/mdx": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "next": { + "optional": true + }, + "shiki": { + "optional": true + } + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "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" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lucide-react": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", + "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz", + "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.38.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", + "integrity": "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.1", + "@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" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.1", + "@next/swc-darwin-x64": "16.2.1", + "@next/swc-linux-arm64-gnu": "16.2.1", + "@next/swc-linux-arm64-musl": "16.2.1", + "@next/swc-linux-x64-gnu": "16.2.1", + "@next/swc-linux-x64-musl": "16.2.1", + "@next/swc-win32-arm64-msvc": "16.2.1", + "@next/swc-win32-x64-msvc": "16.2.1", + "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" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "license": "MIT" + }, + "node_modules/npm-to-yarn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-to-yarn/-/npm-to-yarn-3.0.1.tgz", + "integrity": "sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/nebrelbug/npm-to-yarn?sponsor=1" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", + "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-medium-image-zoom": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/react-medium-image-zoom/-/react-medium-image-zoom-5.4.1.tgz", + "integrity": "sha512-DD2iZYaCfAwiQGR8AN62r/cDJYoXhezlYJc5HY4TzBUGuGge43CptG0f7m0PEIM72aN6GfpjohvY1yYdtCJB7g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/rpearce" + } + ], + "license": "BSD-3-Clause", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", + "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shiki": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", + "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.0.2", + "@shikijs/engine-javascript": "4.0.2", + "@shikijs/engine-oniguruma": "4.0.2", + "@shikijs/langs": "4.0.2", + "@shikijs/themes": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..8eaccf0c --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,32 @@ +{ + "name": "cloudemu-docs", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@tailwindcss/postcss": "^4.2.2", + "autoprefixer": "^10.4.27", + "framer-motion": "^12.38.0", + "fumadocs-core": "^16.7.6", + "fumadocs-mdx": "^14.2.11", + "fumadocs-ui": "^16.7.6", + "lucide-react": "^1.7.0", + "next": "^16.2.1", + "next-themes": "^0.4.6", + "postcss": "^8.5.8", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "tailwindcss": "^4.2.2" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "typescript": "^6.0.2" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 00000000..5d6d8457 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; + +export default config; diff --git a/frontend/source.config.ts b/frontend/source.config.ts new file mode 100644 index 00000000..8dc21074 --- /dev/null +++ b/frontend/source.config.ts @@ -0,0 +1,7 @@ +import { defineDocs, defineConfig } from 'fumadocs-mdx/config'; + +export const { docs, meta } = defineDocs({ + dir: 'content/docs', +}); + +export default defineConfig(); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..7796a883 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.mdx", + ".source/**/*.ts", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} From 2a217528ec7ddb446c4bd9db3f7f29539cb73c2a Mon Sep 17 00:00:00 2001 From: GURSEWAK13 Date: Thu, 26 Mar 2026 15:08:28 +0530 Subject: [PATCH 2/8] =?UTF-8?q?remove=20frontend=20=E2=80=94=20moved=20to?= =?UTF-8?q?=20stackshy/cloudemu-docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.gitignore | 7 - frontend/app/(home)/layout.tsx | 27 - frontend/app/(home)/page.tsx | 39 - frontend/app/api/search/route.ts | 4 - frontend/app/blog/[slug]/page.tsx | 71 - frontend/app/blog/layout.tsx | 27 - frontend/app/blog/page.tsx | 36 - frontend/app/docs/[[...slug]]/page.tsx | 48 - frontend/app/docs/layout.tsx | 30 - frontend/app/global.css | 9 - frontend/app/layout.tsx | 41 - frontend/components/landing/code-example.tsx | 110 - .../components/landing/comparison-table.tsx | 117 - frontend/components/landing/cta-section.tsx | 60 - frontend/components/landing/feature-cards.tsx | 74 - frontend/components/landing/hero.tsx | 98 - frontend/components/landing/service-grid.tsx | 55 - frontend/components/search-dialog.tsx | 62 - frontend/content/blog/hello-world.mdx | 50 - frontend/content/docs/architecture.mdx | 91 - frontend/content/docs/configuration.mdx | 109 - frontend/content/docs/error-handling.mdx | 91 - .../content/docs/features/error-injection.mdx | 71 - frontend/content/docs/features/fake-clock.mdx | 86 - frontend/content/docs/features/index.mdx | 29 - .../docs/features/latency-simulation.mdx | 50 - frontend/content/docs/features/meta.json | 12 - frontend/content/docs/features/metrics.mdx | 54 - .../content/docs/features/rate-limiting.mdx | 39 - frontend/content/docs/features/recording.mdx | 65 - frontend/content/docs/index.mdx | 59 - frontend/content/docs/installation.mdx | 81 - frontend/content/docs/meta.json | 18 - frontend/content/docs/portable-api.mdx | 92 - frontend/content/docs/prerequisites.mdx | 69 - frontend/content/docs/quick-start.mdx | 151 - frontend/content/docs/services/cache.mdx | 44 - frontend/content/docs/services/compute.mdx | 104 - .../docs/services/containerregistry.mdx | 51 - frontend/content/docs/services/database.mdx | 116 - frontend/content/docs/services/dns.mdx | 48 - frontend/content/docs/services/eventbus.mdx | 56 - frontend/content/docs/services/iam.mdx | 71 - frontend/content/docs/services/index.mdx | 55 - .../content/docs/services/loadbalancer.mdx | 57 - frontend/content/docs/services/logging.mdx | 49 - .../content/docs/services/messagequeue.mdx | 74 - frontend/content/docs/services/meta.json | 22 - frontend/content/docs/services/monitoring.mdx | 90 - frontend/content/docs/services/networking.mdx | 91 - .../content/docs/services/notification.mdx | 44 - frontend/content/docs/services/secrets.mdx | 53 - frontend/content/docs/services/serverless.mdx | 75 - frontend/content/docs/services/storage.mdx | 108 - frontend/lib/services.ts | 28 - frontend/lib/source.ts | 8 - frontend/next.config.mjs | 10 - frontend/package-lock.json | 6248 ----------------- frontend/package.json | 32 - frontend/postcss.config.mjs | 8 - frontend/source.config.ts | 7 - frontend/tsconfig.json | 43 - 62 files changed, 9724 deletions(-) delete mode 100644 frontend/.gitignore delete mode 100644 frontend/app/(home)/layout.tsx delete mode 100644 frontend/app/(home)/page.tsx delete mode 100644 frontend/app/api/search/route.ts delete mode 100644 frontend/app/blog/[slug]/page.tsx delete mode 100644 frontend/app/blog/layout.tsx delete mode 100644 frontend/app/blog/page.tsx delete mode 100644 frontend/app/docs/[[...slug]]/page.tsx delete mode 100644 frontend/app/docs/layout.tsx delete mode 100644 frontend/app/global.css delete mode 100644 frontend/app/layout.tsx delete mode 100644 frontend/components/landing/code-example.tsx delete mode 100644 frontend/components/landing/comparison-table.tsx delete mode 100644 frontend/components/landing/cta-section.tsx delete mode 100644 frontend/components/landing/feature-cards.tsx delete mode 100644 frontend/components/landing/hero.tsx delete mode 100644 frontend/components/landing/service-grid.tsx delete mode 100644 frontend/components/search-dialog.tsx delete mode 100644 frontend/content/blog/hello-world.mdx delete mode 100644 frontend/content/docs/architecture.mdx delete mode 100644 frontend/content/docs/configuration.mdx delete mode 100644 frontend/content/docs/error-handling.mdx delete mode 100644 frontend/content/docs/features/error-injection.mdx delete mode 100644 frontend/content/docs/features/fake-clock.mdx delete mode 100644 frontend/content/docs/features/index.mdx delete mode 100644 frontend/content/docs/features/latency-simulation.mdx delete mode 100644 frontend/content/docs/features/meta.json delete mode 100644 frontend/content/docs/features/metrics.mdx delete mode 100644 frontend/content/docs/features/rate-limiting.mdx delete mode 100644 frontend/content/docs/features/recording.mdx delete mode 100644 frontend/content/docs/index.mdx delete mode 100644 frontend/content/docs/installation.mdx delete mode 100644 frontend/content/docs/meta.json delete mode 100644 frontend/content/docs/portable-api.mdx delete mode 100644 frontend/content/docs/prerequisites.mdx delete mode 100644 frontend/content/docs/quick-start.mdx delete mode 100644 frontend/content/docs/services/cache.mdx delete mode 100644 frontend/content/docs/services/compute.mdx delete mode 100644 frontend/content/docs/services/containerregistry.mdx delete mode 100644 frontend/content/docs/services/database.mdx delete mode 100644 frontend/content/docs/services/dns.mdx delete mode 100644 frontend/content/docs/services/eventbus.mdx delete mode 100644 frontend/content/docs/services/iam.mdx delete mode 100644 frontend/content/docs/services/index.mdx delete mode 100644 frontend/content/docs/services/loadbalancer.mdx delete mode 100644 frontend/content/docs/services/logging.mdx delete mode 100644 frontend/content/docs/services/messagequeue.mdx delete mode 100644 frontend/content/docs/services/meta.json delete mode 100644 frontend/content/docs/services/monitoring.mdx delete mode 100644 frontend/content/docs/services/networking.mdx delete mode 100644 frontend/content/docs/services/notification.mdx delete mode 100644 frontend/content/docs/services/secrets.mdx delete mode 100644 frontend/content/docs/services/serverless.mdx delete mode 100644 frontend/content/docs/services/storage.mdx delete mode 100644 frontend/lib/services.ts delete mode 100644 frontend/lib/source.ts delete mode 100644 frontend/next.config.mjs delete mode 100644 frontend/package-lock.json delete mode 100644 frontend/package.json delete mode 100644 frontend/postcss.config.mjs delete mode 100644 frontend/source.config.ts delete mode 100644 frontend/tsconfig.json diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 5b11c9e5..00000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules/ -.next/ -.source/ -out/ -.vercel/ -*.tsbuildinfo -next-env.d.ts diff --git a/frontend/app/(home)/layout.tsx b/frontend/app/(home)/layout.tsx deleted file mode 100644 index 86e56aff..00000000 --- a/frontend/app/(home)/layout.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { HomeLayout } from 'fumadocs-ui/layouts/home'; -import type { ReactNode } from 'react'; - -export default function Layout({ children }: { children: ReactNode }) { - return ( - - cloudemu - - ), - url: '/', - }} - links={[ - { text: 'Docs', url: '/docs' }, - { text: 'Blog', url: '/blog' }, - { - text: 'GitHub', - url: 'https://github.com/stackshy/cloudemu', - }, - ]} - > - {children} - - ); -} diff --git a/frontend/app/(home)/page.tsx b/frontend/app/(home)/page.tsx deleted file mode 100644 index b88e084b..00000000 --- a/frontend/app/(home)/page.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import Link from 'next/link'; -import { Hero } from '@/components/landing/hero'; -import { ComparisonTable } from '@/components/landing/comparison-table'; -import { FeatureCards } from '@/components/landing/feature-cards'; -import { ServiceGrid } from '@/components/landing/service-grid'; -import { CodeExample } from '@/components/landing/code-example'; -import { CTASection } from '@/components/landing/cta-section'; - -export default function HomePage() { - return ( -
    - - - {/* Provider Logos */} -
    -
    -
    - AWS -
    -
    - Azure -
    -
    - GCP -
    -
    -

    - 48 services across 3 cloud providers — all in memory -

    -
    - - - - - - -
    - ); -} diff --git a/frontend/app/api/search/route.ts b/frontend/app/api/search/route.ts deleted file mode 100644 index df889626..00000000 --- a/frontend/app/api/search/route.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { source } from '@/lib/source'; -import { createFromSource } from 'fumadocs-core/search/server'; - -export const { GET } = createFromSource(source); diff --git a/frontend/app/blog/[slug]/page.tsx b/frontend/app/blog/[slug]/page.tsx deleted file mode 100644 index e40a2440..00000000 --- a/frontend/app/blog/[slug]/page.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { notFound } from 'next/navigation'; -import fs from 'fs'; -import path from 'path'; -import type { Metadata } from 'next'; - -const blogDir = path.join(process.cwd(), 'content/blog'); - -async function getBlogPost(slug: string) { - const filePath = path.join(blogDir, `${slug}.mdx`); - if (!fs.existsSync(filePath)) return null; - - const source = fs.readFileSync(filePath, 'utf-8'); - const frontmatterMatch = source.match(/^---\n([\s\S]*?)\n---/); - const content = source.replace(/^---\n[\s\S]*?\n---\n/, ''); - - let title = slug; - let description = ''; - if (frontmatterMatch) { - const fm = frontmatterMatch[1]; - const titleMatch = fm.match(/title:\s*(.*)/); - const descMatch = fm.match(/description:\s*(.*)/); - if (titleMatch) title = titleMatch[1].trim(); - if (descMatch) description = descMatch[1].trim(); - } - - return { title, description, content }; -} - -export default async function BlogPostPage(props: { - params: Promise<{ slug: string }>; -}) { - const params = await props.params; - const post = await getBlogPost(params.slug); - if (!post) notFound(); - - // Simple markdown-like rendering for blog posts - const lines = post.content.split('\n'); - const html = lines - .map((line) => { - if (line.startsWith('# ')) return `

    ${line.slice(2)}

    `; - if (line.startsWith('## ')) return `

    ${line.slice(3)}

    `; - if (line.startsWith('### ')) return `

    ${line.slice(4)}

    `; - if (line.startsWith('- ')) return `
  • ${line.slice(2)}
  • `; - if (line.startsWith('```')) return ''; - if (line.trim() === '') return '
    '; - return `

    ${line}

    `; - }) - .join('\n'); - - return ( -
    -
    -

    {post.title}

    -

    {post.description}

    -
    -
    -
    - ); -} - -export async function generateMetadata(props: { - params: Promise<{ slug: string }>; -}): Promise { - const params = await props.params; - const post = await getBlogPost(params.slug); - if (!post) return {}; - return { title: post.title, description: post.description }; -} diff --git a/frontend/app/blog/layout.tsx b/frontend/app/blog/layout.tsx deleted file mode 100644 index f5fc05fe..00000000 --- a/frontend/app/blog/layout.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { HomeLayout } from 'fumadocs-ui/layouts/home'; -import type { ReactNode } from 'react'; - -export default function BlogLayout({ children }: { children: ReactNode }) { - return ( - - cloudemu - - ), - url: '/', - }} - links={[ - { text: 'Docs', url: '/docs' }, - { text: 'Blog', url: '/blog' }, - { - text: 'GitHub', - url: 'https://github.com/stackshy/cloudemu', - }, - ]} - > - {children} - - ); -} diff --git a/frontend/app/blog/page.tsx b/frontend/app/blog/page.tsx deleted file mode 100644 index e87c9045..00000000 --- a/frontend/app/blog/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import Link from 'next/link'; - -const posts = [ - { - title: 'Introducing cloudemu', - description: 'Zero-cost in-memory cloud emulation for Go', - date: '2026-03-26', - slug: 'hello-world', - }, -]; - -export default function BlogPage() { - return ( -
    -

    Blog

    -

    - Updates, tutorials, and insights from the cloudemu team -

    -
    - {posts.map((post) => ( - - -

    - {post.title} -

    -

    {post.description}

    - - ))} -
    -
    - ); -} diff --git a/frontend/app/docs/[[...slug]]/page.tsx b/frontend/app/docs/[[...slug]]/page.tsx deleted file mode 100644 index efb6ac84..00000000 --- a/frontend/app/docs/[[...slug]]/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { source } from '@/lib/source'; -import { - DocsPage, - DocsBody, - DocsTitle, - DocsDescription, -} from 'fumadocs-ui/page'; -import { notFound } from 'next/navigation'; -import defaultMdxComponents from 'fumadocs-ui/mdx'; -import type { Metadata } from 'next'; - -export default async function Page(props: { - params: Promise<{ slug?: string[] }>; -}) { - const params = await props.params; - const page = source.getPage(params.slug); - if (!page) notFound(); - - const data = page.data as any; - const MDX = data.body; - - return ( - - {data.title} - {data.description} - - - - - ); -} - -export async function generateStaticParams() { - return source.generateParams(); -} - -export async function generateMetadata(props: { - params: Promise<{ slug?: string[] }>; -}): Promise { - const params = await props.params; - const page = source.getPage(params.slug); - if (!page) notFound(); - - return { - title: page.data.title, - description: page.data.description, - }; -} diff --git a/frontend/app/docs/layout.tsx b/frontend/app/docs/layout.tsx deleted file mode 100644 index fc8119c8..00000000 --- a/frontend/app/docs/layout.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { DocsLayout } from 'fumadocs-ui/layouts/docs'; -import type { ReactNode } from 'react'; -import { source } from '@/lib/source'; - -export default function Layout({ children }: { children: ReactNode }) { - return ( - - cloudemu - - ), - url: '/', - }} - sidebar={{ - defaultOpenLevel: 1, - }} - links={[ - { - text: 'Blog', - url: '/blog', - }, - ]} - > - {children} - - ); -} diff --git a/frontend/app/global.css b/frontend/app/global.css deleted file mode 100644 index 1519fb06..00000000 --- a/frontend/app/global.css +++ /dev/null @@ -1,9 +0,0 @@ -@import 'tailwindcss'; -@import 'fumadocs-ui/css/neutral.css'; -@import 'fumadocs-ui/css/preset.css'; - -@theme { - --color-aws: #FF9900; - --color-azure: #0078D4; - --color-gcp: #4285F4; -} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx deleted file mode 100644 index 21981ea7..00000000 --- a/frontend/app/layout.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import './global.css'; -import { RootProvider } from 'fumadocs-ui/provider/next'; -import type { ReactNode } from 'react'; -import type { Metadata } from 'next'; -import CustomSearchDialog from '@/components/search-dialog'; - -export const metadata: Metadata = { - title: { - template: '%s | cloudemu', - default: 'cloudemu — Zero-Cost Cloud Emulation for Go', - }, - description: - 'In-memory cloud service emulation for AWS, Azure, and GCP. No cloud accounts, no Docker, no network calls.', - openGraph: { - title: 'cloudemu — Zero-Cost Cloud Emulation for Go', - description: - 'In-memory cloud service emulation for AWS, Azure, and GCP. No cloud accounts, no Docker, no network calls.', - siteName: 'cloudemu', - }, -}; - -export default function RootLayout({ children }: { children: ReactNode }) { - return ( - - - - {children} - - - - ); -} diff --git a/frontend/components/landing/code-example.tsx b/frontend/components/landing/code-example.tsx deleted file mode 100644 index 767a15a3..00000000 --- a/frontend/components/landing/code-example.tsx +++ /dev/null @@ -1,110 +0,0 @@ -'use client'; - -import { useState } from 'react'; - -const tabs = [ - { - label: 'AWS', - color: '#FF9900', - code: `aws := cloudemu.NewAWS() - -// Launch EC2 instances -instances, _ := aws.EC2.RunInstances(ctx, computedriver.InstanceConfig{ - ImageID: "ami-0abcdef1234", - InstanceType: "t3.large", - Tags: map[string]string{"env": "production"}, -}, 3) - -// Create S3 bucket and upload -aws.S3.CreateBucket(ctx, "app-data") -aws.S3.PutObject(ctx, "app-data", "config.yaml", - []byte("port: 8080"), "text/yaml", nil) - -// Push metrics to CloudWatch -aws.CloudWatch.PutMetricData(ctx, []mondriver.MetricDatum{ - {Namespace: "App", MetricName: "CPU", Value: 45.2}, -})`, - }, - { - label: 'Azure', - color: '#0078D4', - code: `azure := cloudemu.NewAzure() - -// Launch Virtual Machines -instances, _ := azure.VirtualMachines.RunInstances(ctx, - computedriver.InstanceConfig{ - ImageID: "Ubuntu-22.04", - InstanceType: "Standard_D2s_v3", - Tags: map[string]string{"env": "production"}, - }, 3) - -// Create Blob Storage container -azure.BlobStorage.CreateBucket(ctx, "app-data") -azure.BlobStorage.PutObject(ctx, "app-data", "config.yaml", - []byte("port: 8080"), "text/yaml", nil) - -// Push metrics to Azure Monitor -azure.Monitor.PutMetricData(ctx, []mondriver.MetricDatum{ - {Namespace: "App", MetricName: "CPU", Value: 45.2}, -})`, - }, - { - label: 'GCP', - color: '#4285F4', - code: `gcp := cloudemu.NewGCP() - -// Launch GCE instances -instances, _ := gcp.GCE.RunInstances(ctx, - computedriver.InstanceConfig{ - ImageID: "debian-11", - InstanceType: "e2-standard-2", - Tags: map[string]string{"env": "production"}, - }, 3) - -// Create GCS bucket -gcp.GCS.CreateBucket(ctx, "app-data") -gcp.GCS.PutObject(ctx, "app-data", "config.yaml", - []byte("port: 8080"), "text/yaml", nil) - -// Push metrics to Cloud Monitoring -gcp.CloudMonitoring.PutMetricData(ctx, []mondriver.MetricDatum{ - {Namespace: "App", MetricName: "CPU", Value: 45.2}, -})`, - }, -]; - -export function CodeExample() { - const [active, setActive] = useState(0); - - return ( -
    -

    Same API, Every Provider

    -

    - Switch providers by changing one line — your test code stays the same -

    -
    -
    - {tabs.map((tab, i) => ( - - ))} -
    -
    -          
    -            {tabs[active].code}
    -          
    -        
    -
    -
    - ); -} diff --git a/frontend/components/landing/comparison-table.tsx b/frontend/components/landing/comparison-table.tsx deleted file mode 100644 index 55382474..00000000 --- a/frontend/components/landing/comparison-table.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Check, X, Minus } from 'lucide-react'; - -export function ComparisonTable() { - return ( -
    -

    Why cloudemu?

    -

    - Compare approaches to testing cloud-dependent code -

    -
    - - - - - - - - - - - - - - - - - - -
    FeatureReal CloudLocalStack / Emulators - cloudemu -
    -
    -
    - ); -} - -function Row({ - feature, - real, - emulator, - cloudemu, - highlight, - invertBool, -}: { - feature: string; - real: string | boolean; - emulator: string | boolean; - cloudemu: string | boolean; - highlight?: boolean; - invertBool?: boolean; -}) { - const renderCell = (value: string | boolean, isCloudemu = false) => { - if (typeof value === 'boolean') { - const good = invertBool ? !value : value; - return good ? ( - - ) : ( - - ); - } - return ( - - {value} - - ); - }; - - return ( - - {feature} - {renderCell(real)} - {renderCell(emulator)} - - {renderCell(cloudemu, true)} - - - ); -} diff --git a/frontend/components/landing/cta-section.tsx b/frontend/components/landing/cta-section.tsx deleted file mode 100644 index df4bb9ec..00000000 --- a/frontend/components/landing/cta-section.tsx +++ /dev/null @@ -1,60 +0,0 @@ -'use client'; - -import Link from 'next/link'; -import { useState } from 'react'; -import { Copy, Check } from 'lucide-react'; - -export function CTASection() { - const [copied, setCopied] = useState(false); - const installCmd = 'go get github.com/stackshy/cloudemu'; - - const handleCopy = async () => { - await navigator.clipboard.writeText(installCmd); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - return ( -
    -

    Ready to get started?

    -

    - Install cloudemu and start testing your cloud code in seconds -

    - -
    - $ - {installCmd} - -
    - -
    - - Read the Docs - - - Quick Start Guide - -
    - -

    - MIT License · Requires Go 1.25+ -

    -
    - ); -} diff --git a/frontend/components/landing/feature-cards.tsx b/frontend/components/landing/feature-cards.tsx deleted file mode 100644 index 616ed0ce..00000000 --- a/frontend/components/landing/feature-cards.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { - Workflow, - Activity, - AlertTriangle, - Video, - Clock, - Package, -} from 'lucide-react'; - -const features = [ - { - title: 'State Machines', - description: - 'VMs enforce valid lifecycle transitions. Illegal state changes return errors, just like real clouds.', - icon: Workflow, - }, - { - title: 'Auto-Metrics', - description: - 'Launching a VM pushes CPU, Network, and Disk metrics to monitoring. Stop/terminate emit matching values.', - icon: Activity, - }, - { - title: 'Error Injection', - description: - 'Simulate failures deterministically: always, every Nth call, probabilistic, or first N calls.', - icon: AlertTriangle, - }, - { - title: 'Call Recording', - description: - 'Capture every API call with inputs, outputs, errors, and timing. Assert with fluent matchers.', - icon: Video, - }, - { - title: 'Fake Clock', - description: - 'Control time for deterministic testing of TTL, deduplication windows, and alarm evaluation.', - icon: Clock, - }, - { - title: 'Zero Dependencies', - description: - 'Pure Go with no external dependencies. Only testify for tests. Works anywhere Go runs.', - icon: Package, - }, -]; - -export function FeatureCards() { - return ( -
    -

    Beyond Basic Mocks

    -

    - cloudemu reproduces real cloud behaviors so your tests catch real issues -

    -
    - {features.map((feature) => ( -
    -
    - -
    -

    {feature.title}

    -

    - {feature.description} -

    -
    - ))} -
    -
    - ); -} diff --git a/frontend/components/landing/hero.tsx b/frontend/components/landing/hero.tsx deleted file mode 100644 index 063bc3d0..00000000 --- a/frontend/components/landing/hero.tsx +++ /dev/null @@ -1,98 +0,0 @@ -'use client'; - -import Link from 'next/link'; -import { motion } from 'framer-motion'; - -export function Hero() { - return ( -
    - -
    - - Open Source · MIT License -
    - -

    - Zero-Cost{' '} - - Cloud Emulation - -
    - for Go -

    - -

    - No cloud accounts. No Docker. No network calls. -
    - Just go get and test. -

    - -
    - - Get Started - - - GitHub - -
    -
    - - -
    -
    - - - - main.go -
    -
    -            
    -              {'// Create cloud providers — everything runs in memory'}
    -              {'\n'}
    -              aws
    -              {' := '}
    -              cloudemu
    -              .NewAWS()
    -              {'\n'}
    -              azure
    -              {' := '}
    -              cloudemu
    -              .NewAzure()
    -              {'\n'}
    -              gcp
    -              {' := '}
    -              cloudemu
    -              .NewGCP()
    -              {'\n\n'}
    -              {'// Use them exactly like real cloud SDKs'}
    -              {'\n'}
    -              instances
    -              {', _ := '}
    -              aws
    -              .EC2.RunInstances(ctx, config, 
    -              3
    -              )
    -            
    -          
    -
    -
    -
    - ); -} diff --git a/frontend/components/landing/service-grid.tsx b/frontend/components/landing/service-grid.tsx deleted file mode 100644 index 259b1e22..00000000 --- a/frontend/components/landing/service-grid.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import Link from 'next/link'; -import { - Server, HardDrive, Database, Zap, Network, Activity, - Shield, Globe, GitBranch, MessageSquare, Bell, Radio, - Box, MemoryStick, Lock, FileText, -} from 'lucide-react'; -import { services } from '@/lib/services'; - -const iconMap: Record> = { - Server, HardDrive, Database, Zap, Network, Activity, - Shield, Globe, GitBranch, MessageSquare, Bell, Radio, - Box, MemoryStick, Lock, FileText, -}; - -export function ServiceGrid() { - return ( -
    -

    16 Service Categories

    -

    - Every category is implemented for AWS, Azure, and GCP -

    -
    - {services.map((service) => { - const Icon = iconMap[service.icon]; - return ( - -
    - {Icon && } -

    {service.category}

    -
    -
    - - AWS{' '} - {service.aws} - - - Azure{' '} - {service.azure} - - - GCP{' '} - {service.gcp} - -
    - - ); - })} -
    -
    - ); -} diff --git a/frontend/components/search-dialog.tsx b/frontend/components/search-dialog.tsx deleted file mode 100644 index 51cc8291..00000000 --- a/frontend/components/search-dialog.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client'; - -import { useDocsSearch } from 'fumadocs-core/search/client'; -import { - SearchDialog, - SearchDialogOverlay, - SearchDialogContent, - SearchDialogHeader, - SearchDialogIcon, - SearchDialogInput, - SearchDialogClose, - SearchDialogList, - SearchDialogFooter, -} from 'fumadocs-ui/components/dialog/search'; -import type { SharedProps } from 'fumadocs-ui/contexts/search'; - -export default function CustomSearchDialog(props: SharedProps) { - const { search, setSearch, query } = useDocsSearch({ - type: 'fetch', - api: '/api/search', - }); - - const items = - query.data === 'empty' - ? null - : query.data && query.data.length > 0 - ? query.data - : null; - - return ( - - - - - - - - - - search.length > 0 ? ( -
    - No results found for "{search}" -
    - ) : ( -
    - Type to search documentation... -
    - ) - } - /> -
    - -
    - ); -} diff --git a/frontend/content/blog/hello-world.mdx b/frontend/content/blog/hello-world.mdx deleted file mode 100644 index 53e90d7f..00000000 --- a/frontend/content/blog/hello-world.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Introducing cloudemu -description: Zero-cost in-memory cloud emulation for Go ---- - -# Introducing cloudemu - -We're excited to announce **cloudemu** — a Go library that emulates AWS, Azure, and GCP cloud services entirely in memory. - -## The Problem - -Every team that builds on cloud services faces the same testing dilemma: - -1. **Use real cloud accounts** — expensive, slow, requires network access -2. **Use Docker-based emulators** — complex setup, moderate speed, heavy on resources -3. **Write custom mocks** — tedious, incomplete, hard to maintain - -None of these options give you fast, realistic, and free cloud testing. - -## The Solution - -cloudemu provides 48 in-memory service implementations (16 categories across AWS, Azure, and GCP) that behave like the real thing: - -```go -aws := cloudemu.NewAWS() - -// This works exactly like real EC2 -instances, _ := aws.EC2.RunInstances(ctx, config, 3) -``` - -No cloud accounts. No Docker. No network calls. Tests run in ~10ms. - -## What Makes It Different - -cloudemu goes beyond basic CRUD mocks: - -- **State machines** enforce valid lifecycle transitions -- **Auto-metrics** are emitted to the monitoring service -- **FIFO deduplication** with 5-minute windows -- **Dead-letter queues** for messages exceeding max receive count -- **TTL expiry** with lazy cleanup on read -- **IAM policy evaluation** with wildcard matching - -## Get Started - -```bash -go get github.com/stackshy/cloudemu -``` - -Check out the [Quick Start guide](/docs/quick-start) to build your first cloud simulation in 5 minutes. diff --git a/frontend/content/docs/architecture.mdx b/frontend/content/docs/architecture.mdx deleted file mode 100644 index 730f578b..00000000 --- a/frontend/content/docs/architecture.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Architecture -description: Three-layer design inspired by Go CDK ---- - -# Architecture - -cloudemu follows a three-layer architecture inspired by [Go CDK](https://gocloud.dev/): - -``` -Portable API → recording, metrics, rate limiting, error injection -Driver Interface → minimal Go interfaces per service -Provider Mocks → in-memory backends (AWS/Azure/GCP) using generic memstore -``` - -## Layer 1: Provider Mocks - -The bottom layer contains the actual in-memory implementations for each cloud provider. Each provider (AWS, Azure, GCP) implements all 16 service interfaces. - -```go -// providers/aws/aws.go -type Provider struct { - S3 *s3.Mock - EC2 *ec2.Mock - DynamoDB *dynamodb.Mock - Lambda *lambda.Mock - CloudWatch *cloudwatch.Mock - // ... 16 services total -} -``` - -All mocks are backed by a generic, thread-safe `memstore.Store[V]` — a simple in-memory key-value store with `Get`, `Set`, `Delete`, `Filter`, and more. - -Services are wired together at initialization. For example, EC2 is connected to CloudWatch so that launching instances automatically emits CPU, Network, and Disk metrics. - -## Layer 2: Driver Interfaces - -Each service category defines a minimal Go interface that all providers must implement: - -```go -// compute/driver/driver.go -type Compute interface { - RunInstances(ctx context.Context, config InstanceConfig, count int) ([]Instance, error) - StopInstances(ctx context.Context, instanceIDs []string) error - TerminateInstances(ctx context.Context, instanceIDs []string) error - DescribeInstances(ctx context.Context, ids []string, filters []DescribeFilter) ([]Instance, error) - // ... -} -``` - -The driver layer ensures that AWS S3, Azure Blob Storage, and GCP GCS all satisfy the same `driver.Bucket` interface. Your code can work with any of them interchangeably. - -## Layer 3: Portable API - -The top layer wraps driver implementations with cross-cutting concerns: - -```go -bucket := storage.NewBucket(aws.S3, - storage.WithRecorder(rec), // record every API call - storage.WithMetrics(mc), // track call counts and durations - storage.WithErrorInjection(inj), // simulate cloud failures - storage.WithRateLimiter(limiter), // simulate API throttling - storage.WithLatency(5*time.Millisecond),// simulate network delay -) -``` - -The portable API intercepts every call and applies the configured concerns in order: -1. **Error injection** — check if this call should fail -2. **Rate limiting** — check if rate limit is exceeded -3. **Latency** — sleep for configured duration -4. **Execute** — call the underlying driver -5. **Metrics** — record call count, duration, errors -6. **Recording** — log the call details - -## Key Design Decisions - -### Generic memstore - -All providers share the same `memstore.Store[V]` for storage. This ensures consistent thread-safety and behavior across providers. - -### Monitoring auto-wiring - -When a provider is created, services that produce metrics (like EC2) are automatically connected to the monitoring service (like CloudWatch). You can query these metrics the same way you would in production. - -### State machines - -Services with lifecycle states (compute, serverless) use formal state machines that enforce valid transitions. Invalid transitions return `FailedPrecondition` errors. - -### Zero dependencies - -cloudemu has no external dependencies beyond the Go standard library. The only test dependency is `testify`. diff --git a/frontend/content/docs/configuration.mdx b/frontend/content/docs/configuration.mdx deleted file mode 100644 index cd005499..00000000 --- a/frontend/content/docs/configuration.mdx +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: Configuration -description: Configure cloudemu providers with regions, clocks, latency, and more ---- - -# Configuration - -All three providers accept the same functional options from the `config` package. - -```go -import "github.com/stackshy/cloudemu/config" -``` - -## Available Options - -### WithRegion - -Set the cloud region. Defaults to `"us-east-1"`. - -```go -aws := cloudemu.NewAWS( - config.WithRegion("eu-west-1"), -) -``` - -### WithAccountID - -Set the cloud account ID. Defaults to `"123456789012"`. - -```go -aws := cloudemu.NewAWS( - config.WithAccountID("999888777666"), -) -``` - -### WithProjectID - -Set the GCP project ID. Defaults to `"mock-project"`. - -```go -gcp := cloudemu.NewGCP( - config.WithProjectID("my-project-123"), -) -``` - -### WithLatency - -Add simulated network latency to all operations. - -```go -aws := cloudemu.NewAWS( - config.WithLatency(50 * time.Millisecond), -) -``` - -### WithClock - -Control time for deterministic testing. By default, cloudemu uses `RealClock` (system time). - -```go -// Create a fake clock starting at a specific time -clock := config.NewFakeClock(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) - -aws := cloudemu.NewAWS( - config.WithClock(clock), -) - -// Advance time programmatically -clock.Advance(5 * time.Minute) - -// Or set to a specific time -clock.Set(time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)) -``` - -The fake clock is thread-safe and useful for testing: -- TTL expiry in databases and caches -- FIFO deduplication windows in message queues -- Alarm evaluation in monitoring -- Lifecycle policy evaluation in storage - -## Combining Options - -Pass multiple options to any provider: - -```go -clock := config.NewFakeClock(time.Now()) - -aws := cloudemu.NewAWS( - config.WithRegion("eu-west-1"), - config.WithAccountID("999888777666"), - config.WithClock(clock), - config.WithLatency(5 * time.Millisecond), -) - -azure := cloudemu.NewAzure( - config.WithRegion("westeurope"), - config.WithClock(clock), // share the same clock across providers -) -``` - -## Defaults - -| Option | Default Value | -|--------|--------------| -| Region | `"us-east-1"` | -| AccountID | `"123456789012"` | -| ProjectID | `"mock-project"` | -| Clock | `RealClock{}` (system time) | -| Latency | `0` (no delay) | diff --git a/frontend/content/docs/error-handling.mdx b/frontend/content/docs/error-handling.mdx deleted file mode 100644 index e0a890a1..00000000 --- a/frontend/content/docs/error-handling.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Error Handling -description: Canonical error codes and helper functions in cloudemu ---- - -# Error Handling - -All cloudemu operations return errors with canonical codes. Import the errors package: - -```go -import cerrors "github.com/stackshy/cloudemu/errors" -``` - -## Error Codes - -| Code | Description | -|------|-------------| -| `OK` | No error | -| `NotFound` | Resource does not exist | -| `AlreadyExists` | Resource already exists | -| `InvalidArgument` | Invalid input parameter | -| `FailedPrecondition` | Operation rejected due to current state (e.g., invalid state transition) | -| `PermissionDenied` | IAM policy denies the action | -| `Throttled` | Rate limit exceeded | -| `Internal` | Internal error | -| `Unimplemented` | Operation not implemented | -| `ResourceExhausted` | Resource quota exceeded | -| `Unavailable` | Service temporarily unavailable | - -## Checking Error Types - -Use the helper functions to check specific error types: - -```go -_, err := aws.S3.GetObject(ctx, "bucket", "missing-key") -if cerrors.IsNotFound(err) { - // Handle missing resource -} - -err = aws.S3.CreateBucket(ctx, "existing-bucket") -if cerrors.IsAlreadyExists(err) { - // Bucket already exists -} -``` - -### Available Helpers - -```go -cerrors.IsNotFound(err) bool -cerrors.IsAlreadyExists(err) bool -cerrors.IsThrottled(err) bool -cerrors.IsInvalidArgument(err) bool -cerrors.IsFailedPrecondition(err) bool -cerrors.IsPermissionDenied(err) bool -``` - -## Extracting Error Codes - -For error codes without dedicated helpers, use `GetCode`: - -```go -code := cerrors.GetCode(err) - -switch code { -case cerrors.NotFound: - // handle not found -case cerrors.Throttled: - // handle rate limiting -case cerrors.ResourceExhausted: - // handle quota exceeded -default: - // handle other errors -} -``` - -`GetCode` returns: -- `OK` for `nil` errors -- The cloudemu error code for `*cerrors.Error` values -- `Internal` for any other error type - -## Error Format - -Error messages follow the pattern `"Code: message"`: - -```go -err := cerrors.New(cerrors.NotFound, "bucket 'my-bucket' not found") -fmt.Println(err) // "NotFound: bucket 'my-bucket' not found" - -err = cerrors.Newf(cerrors.InvalidArgument, "key %q is empty", "") -fmt.Println(err) // "InvalidArgument: key \"\" is empty" -``` diff --git a/frontend/content/docs/features/error-injection.mdx b/frontend/content/docs/features/error-injection.mdx deleted file mode 100644 index 72a7d4c2..00000000 --- a/frontend/content/docs/features/error-injection.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: Error Injection -description: Simulate cloud failures deterministically ---- - -# Error Injection - -Simulate various failure modes to test your error handling and resilience. - -## Setup - -```go -import "github.com/stackshy/cloudemu/inject" - -inj := inject.NewInjector() - -bucket := storage.NewBucket(aws.S3, - storage.WithErrorInjection(inj), -) -``` - -## Failure Policies - -### Always Fail - -```go -inj.SetPolicy("storage", "PutObject", inject.AlwaysFail( - cerrors.New(cerrors.Unavailable, "service unavailable"), -)) -``` - -### Every Nth Call - -```go -// Fail every 3rd call -inj.SetPolicy("storage", "PutObject", inject.EveryN(3, - cerrors.New(cerrors.Internal, "intermittent failure"), -)) -``` - -### Probabilistic - -```go -// 10% chance of failure -inj.SetPolicy("storage", "PutObject", inject.Probabilistic(0.1, - cerrors.New(cerrors.Internal, "random failure"), -)) -``` - -### First N Calls - -```go -// First 2 calls fail, rest succeed -inj.SetPolicy("storage", "PutObject", inject.FirstN(2, - cerrors.New(cerrors.Unavailable, "warming up"), -)) -``` - -## Clear Policies - -```go -inj.ClearPolicy("storage", "PutObject") -inj.ClearAll() -``` - -## Use Cases - -- Test retry logic with intermittent failures -- Verify timeout handling when services are unavailable -- Simulate cold start failures -- Test circuit breaker patterns diff --git a/frontend/content/docs/features/fake-clock.mdx b/frontend/content/docs/features/fake-clock.mdx deleted file mode 100644 index 84ac3c05..00000000 --- a/frontend/content/docs/features/fake-clock.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Fake Clock -description: Deterministic time control for TTL, dedup, and alarms ---- - -# Fake Clock - -Control time programmatically for deterministic testing. - -## Setup - -```go -import "github.com/stackshy/cloudemu/config" - -clock := config.NewFakeClock(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) - -aws := cloudemu.NewAWS( - config.WithClock(clock), -) -``` - -## Manipulating Time - -```go -// Move forward by a duration -clock.Advance(5 * time.Minute) - -// Jump to a specific time -clock.Set(time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)) - -// Check current fake time -now := clock.Now() -``` - -## Use Cases - -### TTL Expiry - -```go -clock := config.NewFakeClock(time.Now()) -aws := cloudemu.NewAWS(config.WithClock(clock)) - -// Set item with TTL -aws.DynamoDB.PutItem(ctx, "sessions", map[string]any{ - "id": "s-1", "expiresAt": clock.Now().Add(1 * time.Hour).Unix(), -}) - -// Item exists now -item, _ := aws.DynamoDB.GetItem(ctx, "sessions", map[string]any{"id": "s-1"}) -// item != nil - -// Advance past TTL -clock.Advance(2 * time.Hour) - -// Item is now expired -item, _ = aws.DynamoDB.GetItem(ctx, "sessions", map[string]any{"id": "s-1"}) -// item == nil -``` - -### FIFO Deduplication - -```go -// Send a message -aws.SQS.SendMessage(ctx, "queue.fifo", mqdriver.SendMessageInput{ - Body: "order-1", MessageDeduplicationId: "dedup-1", -}) - -// Same dedup ID within 5 minutes — deduplicated -aws.SQS.SendMessage(ctx, "queue.fifo", mqdriver.SendMessageInput{ - Body: "order-1", MessageDeduplicationId: "dedup-1", -}) -// Only 1 message in queue - -// Advance past dedup window -clock.Advance(6 * time.Minute) - -// Now the same ID can be used again -aws.SQS.SendMessage(ctx, "queue.fifo", mqdriver.SendMessageInput{ - Body: "order-1", MessageDeduplicationId: "dedup-1", -}) -// 2 messages in queue -``` - -## Thread Safety - -`FakeClock` is thread-safe — it uses a mutex internally. You can safely share one clock across multiple goroutines and providers. diff --git a/frontend/content/docs/features/index.mdx b/frontend/content/docs/features/index.mdx deleted file mode 100644 index 06c117f2..00000000 --- a/frontend/content/docs/features/index.mdx +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Cross-Cutting Features -description: Recording, metrics, rate limiting, error injection, and more ---- - -# Cross-Cutting Features - -Every cloudemu service can be wrapped with a portable API layer that adds test-oriented features. These features compose together and work identically across all providers. - -```go -bucket := storage.NewBucket(aws.S3, - storage.WithRecorder(rec), // record every API call - storage.WithMetrics(mc), // track call counts and durations - storage.WithErrorInjection(inj), // simulate cloud failures - storage.WithRateLimiter(limiter), // simulate API throttling - storage.WithLatency(5*time.Millisecond),// simulate network delay -) -``` - -## Available Features - -| Feature | What It Does | -|---------|-------------| -| [Call Recording](/docs/features/recording) | Capture every API call with inputs, outputs, errors, and timing | -| [Metrics](/docs/features/metrics) | Track `calls_total`, `call_duration`, `errors_total` per operation | -| [Rate Limiting](/docs/features/rate-limiting) | Token bucket limiter that returns `Throttled` errors when exhausted | -| [Error Injection](/docs/features/error-injection) | Simulate failures: always, every Nth call, probabilistic, or first N calls | -| [Fake Clock](/docs/features/fake-clock) | Control time for deterministic testing of TTL, dedup, alarms | -| [Latency Simulation](/docs/features/latency-simulation) | Add delays to test timeout handling | diff --git a/frontend/content/docs/features/latency-simulation.mdx b/frontend/content/docs/features/latency-simulation.mdx deleted file mode 100644 index 6b9dd909..00000000 --- a/frontend/content/docs/features/latency-simulation.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Latency Simulation -description: Add delays to test timeout handling and performance ---- - -# Latency Simulation - -Add artificial latency to simulate real-world network conditions. - -## Provider-Level Latency - -Add latency to all operations on a provider: - -```go -aws := cloudemu.NewAWS( - config.WithLatency(50 * time.Millisecond), -) -``` - -## Service-Level Latency - -Add latency to specific services using the portable API: - -```go -bucket := storage.NewBucket(aws.S3, - storage.WithLatency(100 * time.Millisecond), -) -``` - -## Use Cases - -- Test timeout handling — verify your code handles slow responses correctly -- Test context cancellation — ensure operations respect `context.WithTimeout` -- Performance testing — measure how your code behaves under various latency conditions - -```go -ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) -defer cancel() - -// This will succeed (100ms < 200ms timeout) -bucket.PutObject(ctx, "bucket", "key", data, "text/plain", nil) - -// With higher latency, it would timeout -slowBucket := storage.NewBucket(aws.S3, - storage.WithLatency(500 * time.Millisecond), -) - -err := slowBucket.PutObject(ctx, "bucket", "key2", data, "text/plain", nil) -// err == context.DeadlineExceeded -``` diff --git a/frontend/content/docs/features/meta.json b/frontend/content/docs/features/meta.json deleted file mode 100644 index 28f456e9..00000000 --- a/frontend/content/docs/features/meta.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "title": "Cross-Cutting Features", - "pages": [ - "index", - "recording", - "metrics", - "rate-limiting", - "error-injection", - "fake-clock", - "latency-simulation" - ] -} diff --git a/frontend/content/docs/features/metrics.mdx b/frontend/content/docs/features/metrics.mdx deleted file mode 100644 index 540e899f..00000000 --- a/frontend/content/docs/features/metrics.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Metrics Collection -description: Track call counts, durations, and errors per operation ---- - -# Metrics Collection - -Collect metrics about API calls for monitoring and assertions. - -## Setup - -```go -import "github.com/stackshy/cloudemu/metrics" - -mc := &metrics.Collector{} - -bucket := storage.NewBucket(aws.S3, - storage.WithMetrics(mc), -) -``` - -## Automatic Metrics - -Every operation automatically records: -- `calls_total` — counter incremented on each call -- `call_duration` — histogram of call durations -- `errors_total` — counter incremented on errors - -All metrics include labels: `service` and `operation`. - -## Querying Metrics - -```go -// Get all counters -counters := mc.Query(metrics.Query{ - Type: metrics.CounterType, - Name: "calls_total", -}) - -// Filter by labels -storagePuts := mc.Query(metrics.Query{ - Type: metrics.CounterType, - Name: "calls_total", - Labels: map[string]string{"operation": "PutObject"}, -}) -``` - -## Metric Types - -| Type | Description | -|------|-------------| -| `CounterType` | Monotonically increasing counter | -| `GaugeType` | Value that can go up and down | -| `HistogramType` | Distribution of values (durations) | diff --git a/frontend/content/docs/features/rate-limiting.mdx b/frontend/content/docs/features/rate-limiting.mdx deleted file mode 100644 index 06011ad1..00000000 --- a/frontend/content/docs/features/rate-limiting.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Rate Limiting -description: Token bucket rate limiter that returns Throttled errors ---- - -# Rate Limiting - -Simulate API rate limits using a token bucket algorithm. - -## Setup - -```go -import "github.com/stackshy/cloudemu/ratelimit" - -limiter := ratelimit.NewLimiter(100) // 100 requests per second - -bucket := storage.NewBucket(aws.S3, - storage.WithRateLimiter(limiter), -) -``` - -## Behavior - -When the rate limit is exceeded, operations return a `Throttled` error: - -```go -import cerrors "github.com/stackshy/cloudemu/errors" - -err := bucket.PutObject(ctx, "bucket", "key", data, "text/plain", nil) -if cerrors.IsThrottled(err) { - // Rate limit exceeded — back off and retry -} -``` - -## Use Cases - -- Test that your retry logic handles rate limiting correctly -- Verify circuit breaker behavior under sustained throttling -- Simulate cloud provider API quotas diff --git a/frontend/content/docs/features/recording.mdx b/frontend/content/docs/features/recording.mdx deleted file mode 100644 index 501eddf9..00000000 --- a/frontend/content/docs/features/recording.mdx +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Call Recording -description: Capture every API call with inputs, outputs, errors, and timing ---- - -# Call Recording - -Record every API call made to a service for test assertions. - -## Setup - -```go -import "github.com/stackshy/cloudemu/recorder" - -rec := &recorder.Recorder{} - -bucket := storage.NewBucket(aws.S3, - storage.WithRecorder(rec), -) -``` - -## Inspecting Calls - -After running your code, inspect all recorded calls: - -```go -calls := rec.Calls() -for _, call := range calls { - fmt.Printf("%s.%s — duration: %v, error: %v\n", - call.Service, call.Operation, call.Duration, call.Error) -} -``` - -Each `Call` contains: -- `Service` — e.g., "storage", "compute" -- `Operation` — e.g., "PutObject", "RunInstances" -- `Input` — the operation input -- `Output` — the operation output -- `Error` — any error returned -- `Timestamp` — when the call was made -- `Duration` — how long it took - -## Fluent Assertions - -Use the `Matcher` for expressive test assertions: - -```go -// Count calls to a specific operation -count := rec.Matcher().Service("storage").Operation("PutObject").Count() -assert.Equal(t, 3, count) - -// Check that no errors occurred -rec.Matcher().Service("storage").AssertNoErrors(t) - -// Filter by service -storageCalls := rec.Matcher().Service("storage").Calls() -``` - -## Reset - -Clear all recorded calls between tests: - -```go -rec.Reset() -``` diff --git a/frontend/content/docs/index.mdx b/frontend/content/docs/index.mdx deleted file mode 100644 index c4562f35..00000000 --- a/frontend/content/docs/index.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Introduction -description: cloudemu is a Go library that emulates cloud services entirely in memory ---- - -# Welcome to cloudemu - -**cloudemu** is a Go library that emulates AWS, Azure, and GCP cloud services entirely in memory. No real cloud accounts, no Docker, no network calls — just import the package, create a provider, and test your cloud code instantly. - -## Why cloudemu? - -Testing cloud-dependent code is painful. You either pay for real accounts, wrestle with heavy emulators that need Docker, or write incomplete mocks from scratch. - -| Approach | Cost | Speed | Offline | -|----------|------|-------|---------| -| Real cloud (AWS/Azure/GCP) | $$$ | Slow (seconds) | No | -| LocalStack / Emulators | $ | Medium (100ms+) | Yes | -| **cloudemu** | **Free** | **Fast (~10ms)** | **Yes** | - -## Quick Example - -```go -package main - -import ( - "context" - "fmt" - "github.com/stackshy/cloudemu" - "github.com/stackshy/cloudemu/compute/driver" -) - -func main() { - ctx := context.Background() - aws := cloudemu.NewAWS() - - instances, _ := aws.EC2.RunInstances(ctx, driver.InstanceConfig{ - ImageID: "ami-0abcdef1234567890", - InstanceType: "t2.micro", - }, 2) - - fmt.Println(instances[0].State) // "running" - fmt.Println(instances[0].ID) // "i-00000001" -} -``` - -This works identically across all three providers. Replace `aws.EC2` with `azure.VirtualMachines` or `gcp.GCE`. - -## What's Included - -- **16 service categories** across 3 cloud providers (48 total implementations) -- **Realistic behaviors**: state machines, auto-metrics, alarm evaluation, FIFO dedup, DLQs, TTL expiry -- **Cross-cutting features**: call recording, metrics collection, error injection, rate limiting, fake clock, latency simulation -- **Zero external dependencies** (only `testify` for tests) - -## Next Steps - -- [Prerequisites](/docs/prerequisites) — what you need before starting -- [Installation](/docs/installation) — add cloudemu to your project -- [Quick Start](/docs/quick-start) — build something in 5 minutes diff --git a/frontend/content/docs/installation.mdx b/frontend/content/docs/installation.mdx deleted file mode 100644 index f3d72405..00000000 --- a/frontend/content/docs/installation.mdx +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Installation -description: How to install cloudemu in your Go project ---- - -# Installation - -## Install the Package - -Add cloudemu to your Go module: - -```bash -go get github.com/stackshy/cloudemu -``` - -This installs the library and all service packages. There are no external dependencies beyond the Go standard library. - -## Verify Installation - -Create a simple test file to verify everything works: - -```go title="main_test.go" -package main - -import ( - "context" - "testing" - - "github.com/stackshy/cloudemu" -) - -func TestCloudemu(t *testing.T) { - ctx := context.Background() - aws := cloudemu.NewAWS() - - err := aws.S3.CreateBucket(ctx, "test-bucket") - if err != nil { - t.Fatal(err) - } - - buckets, err := aws.S3.ListBuckets(ctx) - if err != nil { - t.Fatal(err) - } - - if len(buckets) != 1 { - t.Fatalf("expected 1 bucket, got %d", len(buckets)) - } -} -``` - -Run it: - -```bash -go test -v ./... -``` - -If you see `PASS`, cloudemu is ready to use. - -## Import Paths - -The main package and common sub-packages: - -```go -import ( - "github.com/stackshy/cloudemu" // NewAWS, NewAzure, NewGCP - "github.com/stackshy/cloudemu/config" // WithRegion, WithClock, etc. - cerrors "github.com/stackshy/cloudemu/errors" // Error codes and helpers - "github.com/stackshy/cloudemu/compute/driver" // Compute types - "github.com/stackshy/cloudemu/storage/driver" // Storage types - "github.com/stackshy/cloudemu/database/driver" // Database types - "github.com/stackshy/cloudemu/monitoring/driver" // Monitoring types -) -``` - -Each service has its own `driver` sub-package containing the types and interfaces you need. - -## Next Steps - -- [Quick Start](/docs/quick-start) — build a complete example -- [Configuration](/docs/configuration) — customize your providers diff --git a/frontend/content/docs/meta.json b/frontend/content/docs/meta.json deleted file mode 100644 index 6d09d73b..00000000 --- a/frontend/content/docs/meta.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "title": "Documentation", - "pages": [ - "index", - "prerequisites", - "installation", - "quick-start", - "configuration", - "error-handling", - "architecture", - "---", - "...services", - "---", - "...features", - "---", - "portable-api" - ] -} diff --git a/frontend/content/docs/portable-api.mdx b/frontend/content/docs/portable-api.mdx deleted file mode 100644 index ceb8c310..00000000 --- a/frontend/content/docs/portable-api.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Portable API -description: Wrap any driver with recording, metrics, rate limiting, and error injection ---- - -# Portable API - -The portable API layer wraps driver implementations with test-oriented cross-cutting concerns. Every service category has its own portable wrapper. - -## Basic Usage - -```go -import ( - "github.com/stackshy/cloudemu/storage" - "github.com/stackshy/cloudemu/recorder" - "github.com/stackshy/cloudemu/metrics" - "github.com/stackshy/cloudemu/inject" - "github.com/stackshy/cloudemu/ratelimit" -) - -rec := &recorder.Recorder{} -mc := &metrics.Collector{} -inj := inject.NewInjector() -limiter := ratelimit.NewLimiter(100) // 100 requests/sec - -bucket := storage.NewBucket(aws.S3, - storage.WithRecorder(rec), - storage.WithMetrics(mc), - storage.WithErrorInjection(inj), - storage.WithRateLimiter(limiter), - storage.WithLatency(5 * time.Millisecond), -) - -// Use bucket exactly like aws.S3 — same interface -bucket.CreateBucket(ctx, "my-bucket") -bucket.PutObject(ctx, "my-bucket", "key", data, "text/plain", nil) -``` - -## How It Works - -Every operation passes through a middleware chain: - -1. **Error Injection** — if configured, checks whether the call should fail -2. **Rate Limiting** — if configured, checks the token bucket -3. **Latency** — if configured, sleeps for the specified duration -4. **Driver Call** — executes the actual operation -5. **Metrics** — records `calls_total`, `call_duration`, `errors_total` -6. **Recording** — logs service, operation, input, output, error, duration - -## Available Wrappers - -Each service has its own portable type: - -| Service | Portable Type | Import | -|---------|--------------|--------| -| Storage | `storage.Bucket` | `cloudemu/storage` | -| Compute | `compute.Compute` | `cloudemu/compute` | -| Database | `database.Database` | `cloudemu/database` | -| Serverless | `serverless.Serverless` | `cloudemu/serverless` | -| Monitoring | `monitoring.Monitoring` | `cloudemu/monitoring` | -| Message Queue | `messagequeue.MessageQueue` | `cloudemu/messagequeue` | -| And more... | | | - -All wrappers support the same set of `With*` options. - -## Cross-Provider Testing - -The portable API enables true cross-provider testing: - -```go -func testStorage(t *testing.T, bucket *storage.Bucket) { - ctx := context.Background() - bucket.CreateBucket(ctx, "test") - bucket.PutObject(ctx, "test", "key", []byte("value"), "text/plain", nil) - - obj, err := bucket.GetObject(ctx, "test", "key") - assert.NoError(t, err) - assert.Equal(t, []byte("value"), obj.Data) -} - -func TestAWS(t *testing.T) { - testStorage(t, storage.NewBucket(cloudemu.NewAWS().S3)) -} - -func TestAzure(t *testing.T) { - testStorage(t, storage.NewBucket(cloudemu.NewAzure().BlobStorage)) -} - -func TestGCP(t *testing.T) { - testStorage(t, storage.NewBucket(cloudemu.NewGCP().GCS)) -} -``` diff --git a/frontend/content/docs/prerequisites.mdx b/frontend/content/docs/prerequisites.mdx deleted file mode 100644 index 05e9f799..00000000 --- a/frontend/content/docs/prerequisites.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Prerequisites -description: What you need before using cloudemu ---- - -# Prerequisites - -Before you start using cloudemu, make sure you have the following set up on your machine. - -## Required - -### Go 1.25+ - -cloudemu requires **Go 1.25.0 or later**. Check your version: - -```bash -go version -``` - -If you need to install or upgrade Go, visit [go.dev/dl](https://go.dev/dl/). - -### A Go Module - -Your project should be using Go modules. If you're starting fresh: - -```bash -mkdir my-project && cd my-project -go mod init github.com/yourname/my-project -``` - -## Recommended Knowledge - -### Go Fundamentals - -You should be comfortable with: - -- Writing and running Go programs -- Using `context.Context` for API calls -- Go interfaces and structs -- Error handling patterns (`if err != nil`) -- Go modules and dependency management - -### Cloud Concepts - -While cloudemu handles everything in memory, familiarity with these cloud concepts will help: - -- **Compute**: Virtual machines, instance types, lifecycle states -- **Storage**: Buckets, objects, keys, content types -- **Database**: NoSQL key-value stores, partition keys, queries -- **Networking**: VPCs, subnets, security groups, CIDR blocks -- **Monitoring**: Metrics, alarms, thresholds -- **IAM**: Users, roles, policies, permissions - -You do **not** need: -- A cloud account (AWS, Azure, or GCP) -- Docker or any container runtime -- Network access or internet connection -- Any cloud CLI tools or SDKs - -## Verify Your Setup - -Run this to confirm Go is ready: - -```bash -go version # Should show go1.25.0 or later -go env GOPATH # Should show a valid path -``` - -Once confirmed, proceed to [Installation](/docs/installation). diff --git a/frontend/content/docs/quick-start.mdx b/frontend/content/docs/quick-start.mdx deleted file mode 100644 index 28a28a8e..00000000 --- a/frontend/content/docs/quick-start.mdx +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Quick Start -description: Build a complete cloud simulation in 5 minutes ---- - -# Quick Start - -Let's build a realistic cloud infrastructure simulation — VPC, compute instances, storage, DNS, and monitoring — all in memory. - -## Create an AWS Provider - -```go -package main - -import ( - "context" - "fmt" - - "github.com/stackshy/cloudemu" - computedriver "github.com/stackshy/cloudemu/compute/driver" - netdriver "github.com/stackshy/cloudemu/networking/driver" - storagedriver "github.com/stackshy/cloudemu/storage/driver" -) - -func main() { - ctx := context.Background() - aws := cloudemu.NewAWS() -``` - -## Set Up Networking - -Create a VPC, subnet, and security group: - -```go - // Create a VPC - vpc, _ := aws.VPC.CreateVPC(ctx, netdriver.VPCConfig{ - CIDRBlock: "10.0.0.0/16", - Tags: map[string]string{"env": "production"}, - }) - - // Add a subnet - subnet, _ := aws.VPC.CreateSubnet(ctx, netdriver.SubnetConfig{ - VPCID: vpc.ID, - CIDRBlock: "10.0.1.0/24", - AvailabilityZone: "us-east-1a", - }) - - // Create a security group with HTTPS access - sg, _ := aws.VPC.CreateSecurityGroup(ctx, netdriver.SecurityGroupConfig{ - Name: "web-sg", Description: "Web traffic", VPCID: vpc.ID, - }) - aws.VPC.AddIngressRule(ctx, sg.ID, netdriver.SecurityRule{ - Protocol: "tcp", FromPort: 443, ToPort: 443, CIDR: "0.0.0.0/0", - }) -``` - -## Launch Compute Instances - -```go - // Launch 3 instances — they start in "pending" and transition to "running" - instances, _ := aws.EC2.RunInstances(ctx, computedriver.InstanceConfig{ - ImageID: "ami-0abcdef1234", - InstanceType: "t3.large", - SubnetID: subnet.ID, - SecurityGroups: []string{sg.ID}, - Tags: map[string]string{"app": "web-server"}, - }, 3) - - for _, inst := range instances { - fmt.Printf("Instance %s: state=%s ip=%s\n", - inst.ID, inst.State, inst.PrivateIP) - } - // Instance i-00000001: state=running ip=10.0.0.1 - // Instance i-00000002: state=running ip=10.0.0.2 - // Instance i-00000003: state=running ip=10.0.0.3 -``` - -## Store Objects - -```go - // Create a bucket and upload files - aws.S3.CreateBucket(ctx, "app-deployments") - aws.S3.PutObject(ctx, "app-deployments", "v1.0/app.jar", - []byte("binary-data"), "application/java-archive", nil) - aws.S3.PutObject(ctx, "app-deployments", "v1.0/config.yaml", - []byte("db: rds-prod\nport: 8080"), "text/yaml", nil) - - // List objects by prefix - result, _ := aws.S3.ListObjects(ctx, "app-deployments", - storagedriver.ListOptions{Prefix: "v1.0/"}) - fmt.Printf("Objects in v1.0/: %d\n", len(result.Objects)) - - // Retrieve an object - obj, _ := aws.S3.GetObject(ctx, "app-deployments", "v1.0/config.yaml") - fmt.Printf("Config: %s\n", string(obj.Data)) -``` - -## Instance Lifecycle - -```go - // Stop an instance — state machine enforces valid transitions - aws.EC2.StopInstances(ctx, []string{instances[0].ID}) - - // Modify while stopped (resize) - aws.EC2.ModifyInstance(ctx, instances[0].ID, computedriver.ModifyInstanceInput{ - InstanceType: "t3.xlarge", - }) - - // Start it back - aws.EC2.StartInstances(ctx, []string{instances[0].ID}) - - // Terminate all - for _, inst := range instances { - aws.EC2.TerminateInstances(ctx, []string{inst.ID}) - } - - // Trying to stop a terminated instance returns an error - err := aws.EC2.StopInstances(ctx, []string{instances[0].ID}) - fmt.Println(err) // "cannot stop instance: invalid transition" -} -``` - -## Run It - -```bash -go run main.go -``` - -Everything runs in memory. No cloud account needed. No Docker. Zero cost. - -## Try Other Providers - -The same code works with Azure and GCP — just change the provider: - -```go -// Azure -azure := cloudemu.NewAzure() -azure.VirtualMachines.RunInstances(ctx, config, 3) -azure.BlobStorage.CreateBucket(ctx, "my-bucket") - -// GCP -gcp := cloudemu.NewGCP() -gcp.GCE.RunInstances(ctx, config, 3) -gcp.GCS.CreateBucket(ctx, "my-bucket") -``` - -## Next Steps - -- [Configuration](/docs/configuration) — customize regions, clocks, and more -- [Services](/docs/services) — explore all 16 service categories -- [Cross-Cutting Features](/docs/features) — add recording, metrics, and error injection diff --git a/frontend/content/docs/services/cache.mdx b/frontend/content/docs/services/cache.mdx deleted file mode 100644 index e4d6767f..00000000 --- a/frontend/content/docs/services/cache.mdx +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Cache -description: In-memory cache with TTL support ---- - -# Cache - -Emulates cache services: **ElastiCache** (AWS), **Cache** (Azure), **Memorystore** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | ElastiCache | `aws.ElastiCache` | -| Azure | Cache for Redis | `azure.Cache` | -| GCP | Memorystore | `gcp.Memorystore` | - -## Key Operations - -### Basic Operations - -```go -import cachedriver "github.com/stackshy/cloudemu/cache/driver" - -// Create a cache instance -aws.ElastiCache.CreateCacheInstance(ctx, cachedriver.CacheConfig{ - Name: "session-cache", - NodeType: "cache.t3.micro", -}) - -// Set a value with TTL -aws.ElastiCache.Set(ctx, "session-cache", "user:123", []byte("session-data"), 30*time.Minute) - -// Get a value -data, _ := aws.ElastiCache.Get(ctx, "session-cache", "user:123") - -// Delete -aws.ElastiCache.Delete(ctx, "session-cache", "user:123") -``` - -## Realistic Behaviors - -- **TTL expiry**: cached items are automatically expired after their TTL, with lazy cleanup on read -- **Thread-safe**: all cache operations are safe for concurrent access diff --git a/frontend/content/docs/services/compute.mdx b/frontend/content/docs/services/compute.mdx deleted file mode 100644 index 95e6759a..00000000 --- a/frontend/content/docs/services/compute.mdx +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: Compute -description: Virtual machine instances with lifecycle state machines ---- - -# Compute - -Emulates virtual machine services: **EC2** (AWS), **VirtualMachines** (Azure), **GCE** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | EC2 | `aws.EC2` | -| Azure | Virtual Machines | `azure.VirtualMachines` | -| GCP | GCE | `gcp.GCE` | - -## Key Operations - -### Instance Lifecycle - -```go -// Launch instances -instances, _ := aws.EC2.RunInstances(ctx, driver.InstanceConfig{ - ImageID: "ami-0abcdef1234", - InstanceType: "t3.large", - SubnetID: "subnet-123", - Tags: map[string]string{"env": "prod"}, -}, 3) - -// Stop, start, reboot -aws.EC2.StopInstances(ctx, []string{"i-00000001"}) -aws.EC2.StartInstances(ctx, []string{"i-00000001"}) -aws.EC2.RebootInstances(ctx, []string{"i-00000001"}) - -// Modify while stopped -aws.EC2.ModifyInstance(ctx, "i-00000001", driver.ModifyInstanceInput{ - InstanceType: "t3.xlarge", -}) - -// Terminate -aws.EC2.TerminateInstances(ctx, []string{"i-00000001"}) -``` - -### Describe and Filter - -```go -// Get specific instances -instances, _ := aws.EC2.DescribeInstances(ctx, []string{"i-00000001"}, nil) - -// Filter by state -running, _ := aws.EC2.DescribeInstances(ctx, nil, []driver.DescribeFilter{ - {Name: "instance-state-name", Values: []string{"running"}}, -}) -``` - -### Auto-Scaling Groups - -```go -asg, _ := aws.EC2.CreateAutoScalingGroup(ctx, driver.AutoScalingGroupConfig{ - Name: "web-asg", MinSize: 1, MaxSize: 10, DesiredCapacity: 3, - InstanceConfig: driver.InstanceConfig{ImageID: "ami-123", InstanceType: "t3.micro"}, -}) - -aws.EC2.SetDesiredCapacity(ctx, "web-asg", 5) -``` - -### Spot Instances - -```go -requests, _ := aws.EC2.RequestSpotInstances(ctx, driver.SpotRequestConfig{ - InstanceConfig: driver.InstanceConfig{ImageID: "ami-123", InstanceType: "t3.micro"}, - MaxPrice: 0.05, Count: 2, Type: "one-time", -}) -``` - -### Launch Templates - -```go -template, _ := aws.EC2.CreateLaunchTemplate(ctx, driver.LaunchTemplateConfig{ - Name: "web-template", - InstanceConfig: driver.InstanceConfig{ImageID: "ami-123", InstanceType: "t3.large"}, -}) -``` - -## State Machine - -Instances follow a strict state machine: - -``` -pending → running → stopping → stopped → starting → running - → shutting-down → terminated -``` - -Invalid transitions return `FailedPrecondition` errors. For example, you cannot stop a terminated instance. - -## Auto-Metrics - -When instances are launched, cloudemu automatically pushes metrics to the monitoring service: -- `CPUUtilization` — per instance -- `NetworkIn` / `NetworkOut` — per instance -- `DiskReadOps` / `DiskWriteOps` — per instance - -These metrics are queryable through `CloudWatch.GetMetricData()`. diff --git a/frontend/content/docs/services/containerregistry.mdx b/frontend/content/docs/services/containerregistry.mdx deleted file mode 100644 index d3f2715f..00000000 --- a/frontend/content/docs/services/containerregistry.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Container Registry -description: Container image storage, lifecycle policies, and scanning ---- - -# Container Registry - -Emulates container registries: **ECR** (AWS), **ACR** (Azure), **ArtifactRegistry** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | ECR | `aws.ECR` | -| Azure | ACR | `azure.ACR` | -| GCP | Artifact Registry | `gcp.ArtifactRegistry` | - -## Key Operations - -### Repositories - -```go -import crdriver "github.com/stackshy/cloudemu/containerregistry/driver" - -aws.ECR.CreateRepository(ctx, crdriver.RepositoryConfig{ - Name: "my-app", - Tags: map[string]string{"team": "platform"}, -}) - -repos, _ := aws.ECR.ListRepositories(ctx) -``` - -### Image Management - -```go -aws.ECR.PushImage(ctx, crdriver.PushImageInput{ - Repository: "my-app", - Tag: "v1.0.0", - Digest: "sha256:abc123...", -}) - -images, _ := aws.ECR.ListImages(ctx, "my-app") -``` - -### Lifecycle Policies - -Configure policies to automatically clean up old or untagged images. - -### Image Scanning - -Trigger vulnerability scans on pushed images. diff --git a/frontend/content/docs/services/database.mdx b/frontend/content/docs/services/database.mdx deleted file mode 100644 index b6356655..00000000 --- a/frontend/content/docs/services/database.mdx +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: Database -description: NoSQL database with queries, TTL, and change streams ---- - -# Database - -Emulates NoSQL databases: **DynamoDB** (AWS), **CosmosDB** (Azure), **Firestore** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | DynamoDB | `aws.DynamoDB` | -| Azure | CosmosDB | `azure.CosmosDB` | -| GCP | Firestore | `gcp.Firestore` | - -## Key Operations - -### Table Management - -```go -aws.DynamoDB.CreateTable(ctx, driver.TableConfig{ - Name: "users", - PartitionKey: "userId", - SortKey: "email", -}) - -tables, _ := aws.DynamoDB.ListTables(ctx) -``` - -### Item Operations - -```go -// Put -aws.DynamoDB.PutItem(ctx, "users", map[string]any{ - "userId": "u-001", "email": "alice@example.com", "name": "Alice", "age": 30, -}) - -// Get -item, _ := aws.DynamoDB.GetItem(ctx, "users", map[string]any{ - "userId": "u-001", "email": "alice@example.com", -}) - -// Delete -aws.DynamoDB.DeleteItem(ctx, "users", map[string]any{ - "userId": "u-001", "email": "alice@example.com", -}) - -// Batch operations -aws.DynamoDB.BatchPutItems(ctx, "users", []map[string]any{item1, item2, item3}) -items, _ := aws.DynamoDB.BatchGetItems(ctx, "users", []map[string]any{key1, key2}) -``` - -### Query and Scan - -```go -// Query by key condition -result, _ := aws.DynamoDB.Query(ctx, driver.QueryInput{ - Table: "users", - KeyCondition: driver.KeyCondition{ - PartitionKey: "userId", PartitionVal: "u-001", - SortOp: "BEGINS_WITH", SortVal: "a", - }, - Limit: 10, -}) - -// Scan with filters -result, _ = aws.DynamoDB.Scan(ctx, driver.ScanInput{ - Table: "users", - Filters: []driver.ScanFilter{ - {Field: "age", Op: ">", Value: 25}, - }, -}) -``` - -### TTL - -```go -aws.DynamoDB.UpdateTTL(ctx, "sessions", driver.TTLConfig{ - Enabled: true, - AttributeName: "expiresAt", -}) -``` - -Items with an `expiresAt` timestamp in the past are automatically cleaned up on read. - -### Streams / Change Feed - -```go -aws.DynamoDB.UpdateStreamConfig(ctx, "users", driver.StreamConfig{ - Enabled: true, - ViewType: "NEW_AND_OLD_IMAGES", -}) - -// After mutations, read the stream -iter, _ := aws.DynamoDB.GetStreamRecords(ctx, "users", 100, "") -for _, record := range iter.Records { - fmt.Println(record.EventType) // "INSERT", "MODIFY", or "REMOVE" -} -``` - -### Transactions - -```go -aws.DynamoDB.TransactWriteItems(ctx, "users", - []map[string]any{newItem1, newItem2}, // puts - []map[string]any{deleteKey1}, // deletes -) -``` - -## Realistic Behaviors - -- **Numeric-aware comparisons**: filters compare `"10" > "9"` correctly -- **TTL lazy cleanup**: expired items are removed on read, not in the background -- **Stream records**: every mutation (INSERT/MODIFY/REMOVE) produces a stream record with old and new images diff --git a/frontend/content/docs/services/dns.mdx b/frontend/content/docs/services/dns.mdx deleted file mode 100644 index d1873557..00000000 --- a/frontend/content/docs/services/dns.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: DNS -description: DNS zones and records with weighted routing ---- - -# DNS - -Emulates DNS services: **Route53** (AWS), **DNS** (Azure), **CloudDNS** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | Route53 | `aws.Route53` | -| Azure | DNS | `azure.DNS` | -| GCP | Cloud DNS | `gcp.CloudDNS` | - -## Key Operations - -### Zones - -```go -import dnsdriver "github.com/stackshy/cloudemu/dns/driver" - -zone, _ := aws.Route53.CreateZone(ctx, dnsdriver.ZoneConfig{ - Name: "example.com", -}) - -zones, _ := aws.Route53.ListZones(ctx) -``` - -### Records - -```go -aws.Route53.CreateRecord(ctx, dnsdriver.RecordConfig{ - ZoneID: zone.ID, - Name: "api.example.com", - Type: "A", - TTL: 300, - Values: []string{"10.0.0.1", "10.0.0.2"}, -}) - -records, _ := aws.Route53.ListRecords(ctx, zone.ID) -``` - -### Weighted Routing - -DNS records support weighted routing for load distribution across multiple endpoints. diff --git a/frontend/content/docs/services/eventbus.mdx b/frontend/content/docs/services/eventbus.mdx deleted file mode 100644 index 25ecdcfd..00000000 --- a/frontend/content/docs/services/eventbus.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Event Bus -description: Event routing with rules and targets ---- - -# Event Bus - -Emulates event routing: **EventBridge** (AWS), **EventGrid** (Azure), **Eventarc** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | EventBridge | `aws.EventBridge` | -| Azure | Event Grid | `azure.EventGrid` | -| GCP | Eventarc | `gcp.Eventarc` | - -## Key Operations - -### Event Buses - -```go -import ebdriver "github.com/stackshy/cloudemu/eventbus/driver" - -bus, _ := aws.EventBridge.CreateEventBus(ctx, ebdriver.EventBusConfig{ - Name: "app-events", -}) -``` - -### Rules and Targets - -```go -aws.EventBridge.CreateRule(ctx, ebdriver.RuleConfig{ - EventBusID: bus.ID, - Name: "order-rule", - Pattern: `{"source": ["orders"]}`, -}) - -aws.EventBridge.AddTarget(ctx, ebdriver.TargetConfig{ - RuleID: "order-rule", - TargetID: "process-order", -}) -``` - -### Publishing Events - -```go -aws.EventBridge.PutEvents(ctx, []ebdriver.Event{ - { - Source: "orders", - DetailType: "OrderCreated", - Detail: `{"orderId": "123"}`, - EventBusID: bus.ID, - }, -}) -``` diff --git a/frontend/content/docs/services/iam.mdx b/frontend/content/docs/services/iam.mdx deleted file mode 100644 index 2c126420..00000000 --- a/frontend/content/docs/services/iam.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: IAM -description: Identity, roles, and policy evaluation with wildcard matching ---- - -# IAM - -Emulates identity and access management across all three providers. - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | IAM | `aws.IAM` | -| Azure | IAM | `azure.IAM` | -| GCP | IAM | `gcp.IAM` | - -## Key Operations - -### Users and Roles - -```go -import iamdriver "github.com/stackshy/cloudemu/iam/driver" - -// Create a user -aws.IAM.CreateUser(ctx, iamdriver.UserConfig{ - Name: "alice", Tags: map[string]string{"team": "backend"}, -}) - -// Create a role -aws.IAM.CreateRole(ctx, iamdriver.RoleConfig{ - Name: "s3-reader", - AssumeRolePolicy: `{"Version":"2012-10-17","Statement":[...]}`, -}) -``` - -### Policies - -```go -// Attach a policy -aws.IAM.AttachPolicy(ctx, iamdriver.AttachPolicyInput{ - TargetType: "user", - TargetName: "alice", - PolicyDocument: `{ - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": "s3:GetObject", - "Resource": "arn:aws:s3:::my-bucket/*" - }] - }`, -}) -``` - -### Permission Checking - -```go -allowed, _ := aws.IAM.CheckPermission(ctx, iamdriver.PermissionCheck{ - Principal: "alice", - Action: "s3:GetObject", - Resource: "arn:aws:s3:::my-bucket/file.txt", -}) -// allowed == true -``` - -## Policy Evaluation - -cloudemu parses JSON policy documents with full support for: -- **Wildcard matching** in actions and resources (`s3:*`, `arn:aws:s3:::*`) -- **Explicit Deny overrides Allow** — matching real IAM behavior -- **Multiple statements** with different effects diff --git a/frontend/content/docs/services/index.mdx b/frontend/content/docs/services/index.mdx deleted file mode 100644 index 0d33dcc8..00000000 --- a/frontend/content/docs/services/index.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Services Overview -description: All 16 service categories across AWS, Azure, and GCP ---- - -# Services Overview - -cloudemu provides **16 service categories**, each implemented for all three cloud providers — **48 total implementations**. - -## Service Mapping - -| Category | AWS | Azure | GCP | -|----------|-----|-------|-----| -| [Compute](/docs/services/compute) | EC2 | VirtualMachines | GCE | -| [Storage](/docs/services/storage) | S3 | BlobStorage | GCS | -| [Database](/docs/services/database) | DynamoDB | CosmosDB | Firestore | -| [Serverless](/docs/services/serverless) | Lambda | Functions | CloudFunctions | -| [Networking](/docs/services/networking) | VPC | VNet | VPC | -| [Monitoring](/docs/services/monitoring) | CloudWatch | Monitor | CloudMonitoring | -| [IAM](/docs/services/iam) | IAM | IAM | IAM | -| [DNS](/docs/services/dns) | Route53 | DNS | CloudDNS | -| [Load Balancer](/docs/services/loadbalancer) | ELB | LB | LB | -| [Message Queue](/docs/services/messagequeue) | SQS | ServiceBus | PubSub | -| [Notification](/docs/services/notification) | SNS | NotificationHubs | FCM | -| [Event Bus](/docs/services/eventbus) | EventBridge | EventGrid | Eventarc | -| [Container Registry](/docs/services/containerregistry) | ECR | ACR | ArtifactRegistry | -| [Cache](/docs/services/cache) | ElastiCache | Cache | Memorystore | -| [Secrets](/docs/services/secrets) | SecretsManager | KeyVault | SecretManager | -| [Logging](/docs/services/logging) | CloudWatchLogs | LogAnalytics | CloudLogging | - -## Accessing Services - -Each provider exposes services as typed fields: - -```go -aws := cloudemu.NewAWS() -aws.EC2 // Compute -aws.S3 // Storage -aws.DynamoDB // Database -aws.Lambda // Serverless -aws.VPC // Networking -aws.CloudWatch // Monitoring - -azure := cloudemu.NewAzure() -azure.VirtualMachines // Compute -azure.BlobStorage // Storage -azure.CosmosDB // Database - -gcp := cloudemu.NewGCP() -gcp.GCE // Compute -gcp.GCS // Storage -gcp.Firestore // Database -``` - -All providers implement the same driver interfaces, so the operations and behaviors are consistent across clouds. diff --git a/frontend/content/docs/services/loadbalancer.mdx b/frontend/content/docs/services/loadbalancer.mdx deleted file mode 100644 index cfea1f62..00000000 --- a/frontend/content/docs/services/loadbalancer.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: Load Balancer -description: Load balancers, target groups, listeners, and health checks ---- - -# Load Balancer - -Emulates load balancing: **ELB** (AWS), **LB** (Azure), **LB** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | ELB | `aws.ELB` | -| Azure | LB | `azure.LB` | -| GCP | LB | `gcp.LB` | - -## Key Operations - -### Load Balancers - -```go -import lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver" - -lb, _ := aws.ELB.CreateLoadBalancer(ctx, lbdriver.LoadBalancerConfig{ - Name: "web-lb", - Type: "application", - Scheme: "internet-facing", -}) -``` - -### Target Groups - -```go -tg, _ := aws.ELB.CreateTargetGroup(ctx, lbdriver.TargetGroupConfig{ - Name: "web-targets", - Port: 8080, - Protocol: "HTTP", -}) - -aws.ELB.RegisterTargets(ctx, tg.ID, []string{"i-00000001", "i-00000002"}) -``` - -### Listeners - -```go -aws.ELB.CreateListener(ctx, lbdriver.ListenerConfig{ - LoadBalancerID: lb.ID, - Port: 443, - Protocol: "HTTPS", - TargetGroupID: tg.ID, -}) -``` - -### Health Checks - -Target groups include health check configuration that determines target health status. diff --git a/frontend/content/docs/services/logging.mdx b/frontend/content/docs/services/logging.mdx deleted file mode 100644 index ccb04a4b..00000000 --- a/frontend/content/docs/services/logging.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Logging -description: Log groups and log streams ---- - -# Logging - -Emulates logging services: **CloudWatch Logs** (AWS), **Log Analytics** (Azure), **Cloud Logging** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | CloudWatch Logs | `aws.CloudWatchLogs` | -| Azure | Log Analytics | `azure.LogAnalytics` | -| GCP | Cloud Logging | `gcp.CloudLogging` | - -## Key Operations - -### Log Groups and Streams - -```go -import logdriver "github.com/stackshy/cloudemu/logging/driver" - -// Create a log group -aws.CloudWatchLogs.CreateLogGroup(ctx, logdriver.LogGroupConfig{ - Name: "/app/web", - RetentionDays: 30, -}) - -// Create a log stream -aws.CloudWatchLogs.CreateLogStream(ctx, "/app/web", "instance-001") - -// Put log events -aws.CloudWatchLogs.PutLogEvents(ctx, "/app/web", "instance-001", []logdriver.LogEvent{ - {Timestamp: time.Now(), Message: "Server started on port 8080"}, - {Timestamp: time.Now(), Message: "Handling request GET /api/users"}, -}) -``` - -### Querying Logs - -```go -events, _ := aws.CloudWatchLogs.GetLogEvents(ctx, "/app/web", "instance-001", - logdriver.GetLogEventsInput{ - StartTime: time.Now().Add(-1 * time.Hour), - Limit: 100, - }) -``` diff --git a/frontend/content/docs/services/messagequeue.mdx b/frontend/content/docs/services/messagequeue.mdx deleted file mode 100644 index fe98c903..00000000 --- a/frontend/content/docs/services/messagequeue.mdx +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: Message Queue -description: Queues with FIFO deduplication and dead-letter queues ---- - -# Message Queue - -Emulates message queues: **SQS** (AWS), **ServiceBus** (Azure), **PubSub** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | SQS | `aws.SQS` | -| Azure | Service Bus | `azure.ServiceBus` | -| GCP | Pub/Sub | `gcp.PubSub` | - -## Key Operations - -### Queue Management - -```go -import mqdriver "github.com/stackshy/cloudemu/messagequeue/driver" - -aws.SQS.CreateQueue(ctx, mqdriver.QueueConfig{ - Name: "orders", - Attributes: map[string]string{ - "VisibilityTimeout": "30", - }, -}) - -// FIFO queue -aws.SQS.CreateQueue(ctx, mqdriver.QueueConfig{ - Name: "orders.fifo", - FIFOQueue: true, -}) -``` - -### Send and Receive Messages - -```go -// Send -aws.SQS.SendMessage(ctx, "orders", mqdriver.SendMessageInput{ - Body: "order-123", - Attributes: map[string]string{"priority": "high"}, -}) - -// Receive -messages, _ := aws.SQS.ReceiveMessages(ctx, "orders", mqdriver.ReceiveInput{ - MaxMessages: 10, - WaitTimeSeconds: 0, -}) - -// Delete after processing -aws.SQS.DeleteMessage(ctx, "orders", messages[0].ReceiptHandle) -``` - -### Batch Operations - -```go -aws.SQS.SendMessageBatch(ctx, "orders", []mqdriver.SendMessageInput{ - {Body: "order-1"}, {Body: "order-2"}, {Body: "order-3"}, -}) -``` - -### Dead-Letter Queues - -Configure a DLQ and messages exceeding the max receive count automatically move there. - -## Realistic Behaviors - -- **FIFO deduplication**: FIFO queues enforce 5-minute deduplication windows using `MessageDeduplicationId` -- **Visibility timeout**: received messages are invisible to other consumers until timeout expires or message is deleted -- **Dead-letter queues**: messages exceeding max receive count are automatically moved to the DLQ diff --git a/frontend/content/docs/services/meta.json b/frontend/content/docs/services/meta.json deleted file mode 100644 index 6f5b3a1b..00000000 --- a/frontend/content/docs/services/meta.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "title": "Services", - "pages": [ - "index", - "compute", - "storage", - "database", - "serverless", - "networking", - "monitoring", - "iam", - "dns", - "loadbalancer", - "messagequeue", - "notification", - "eventbus", - "containerregistry", - "cache", - "secrets", - "logging" - ] -} diff --git a/frontend/content/docs/services/monitoring.mdx b/frontend/content/docs/services/monitoring.mdx deleted file mode 100644 index 0d20872c..00000000 --- a/frontend/content/docs/services/monitoring.mdx +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Monitoring -description: Metrics, alarms, and metric queries ---- - -# Monitoring - -Emulates monitoring services: **CloudWatch** (AWS), **Monitor** (Azure), **CloudMonitoring** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | CloudWatch | `aws.CloudWatch` | -| Azure | Monitor | `azure.Monitor` | -| GCP | Cloud Monitoring | `gcp.CloudMonitoring` | - -## Key Operations - -### Push Metrics - -```go -import mondriver "github.com/stackshy/cloudemu/monitoring/driver" - -aws.CloudWatch.PutMetricData(ctx, []mondriver.MetricDatum{ - { - Namespace: "App/Web", - MetricName: "CPUUtilization", - Value: 72.8, - Timestamp: time.Now(), - Dimensions: map[string]string{"InstanceId": "i-00000001"}, - }, - { - Namespace: "App/Web", - MetricName: "RequestCount", - Value: 15230, - Timestamp: time.Now(), - }, -}) -``` - -### Query Metrics - -```go -result, _ := aws.CloudWatch.GetMetricData(ctx, mondriver.GetMetricInput{ - Namespace: "App/Web", - MetricName: "CPUUtilization", - Dimensions: map[string]string{"InstanceId": "i-00000001"}, - StartTime: time.Now().Add(-5 * time.Minute), - EndTime: time.Now(), - Period: 60, - Stat: "Average", // also: "Sum", "Minimum", "Maximum", "SampleCount" -}) - -fmt.Printf("CPU: %.1f%%\n", result.Values[0]) -``` - -### List Metrics - -```go -metrics, _ := aws.CloudWatch.ListMetrics(ctx, "App/Web") -// ["CPUUtilization", "RequestCount"] -``` - -### Alarms - -```go -// Create an alarm -aws.CloudWatch.CreateAlarm(ctx, mondriver.AlarmConfig{ - Name: "high-cpu", - Namespace: "App/Web", - MetricName: "CPUUtilization", - ComparisonOperator: "GreaterThanThreshold", - Threshold: 80, - Period: 300, - EvaluationPeriods: 2, - Stat: "Average", -}) - -// List alarms -alarms, _ := aws.CloudWatch.DescribeAlarms(ctx, nil) -``` - -## Auto-Metrics - -Services like EC2 automatically push metrics to the monitoring service when instances are launched, stopped, or terminated. You can query these exactly as you would in production. - -## Alarm Evaluation - -Alarms automatically evaluate when new metric data is pushed. They transition between `OK` and `ALARM` states based on threshold comparison. diff --git a/frontend/content/docs/services/networking.mdx b/frontend/content/docs/services/networking.mdx deleted file mode 100644 index 50187f73..00000000 --- a/frontend/content/docs/services/networking.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Networking -description: Virtual networks, subnets, security groups, and peering ---- - -# Networking - -Emulates virtual networking: **VPC** (AWS), **VNet** (Azure), **VPC** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | VPC | `aws.VPC` | -| Azure | VNet | `azure.VNet` | -| GCP | VPC | `gcp.VPC` | - -## Key Operations - -### VPCs / Virtual Networks - -```go -import netdriver "github.com/stackshy/cloudemu/networking/driver" - -vpc, _ := aws.VPC.CreateVPC(ctx, netdriver.VPCConfig{ - CIDRBlock: "10.0.0.0/16", - Tags: map[string]string{"env": "production"}, -}) -``` - -### Subnets - -```go -subnet, _ := aws.VPC.CreateSubnet(ctx, netdriver.SubnetConfig{ - VPCID: vpc.ID, - CIDRBlock: "10.0.1.0/24", - AvailabilityZone: "us-east-1a", -}) -``` - -### Security Groups - -```go -sg, _ := aws.VPC.CreateSecurityGroup(ctx, netdriver.SecurityGroupConfig{ - Name: "web-sg", Description: "Web traffic", VPCID: vpc.ID, -}) - -// Add ingress rule -aws.VPC.AddIngressRule(ctx, sg.ID, netdriver.SecurityRule{ - Protocol: "tcp", FromPort: 443, ToPort: 443, CIDR: "0.0.0.0/0", -}) - -// Add egress rule -aws.VPC.AddEgressRule(ctx, sg.ID, netdriver.SecurityRule{ - Protocol: "tcp", FromPort: 0, ToPort: 65535, CIDR: "0.0.0.0/0", -}) -``` - -### VPC Peering - -```go -peering, _ := aws.VPC.CreatePeeringConnection(ctx, netdriver.PeeringConfig{ - RequesterVPCID: vpc1.ID, AccepterVPCID: vpc2.ID, -}) -aws.VPC.AcceptPeeringConnection(ctx, peering.ID) -``` - -### NAT Gateways - -```go -nat, _ := aws.VPC.CreateNATGateway(ctx, netdriver.NATGatewayConfig{ - SubnetID: subnet.ID, -}) -``` - -### Route Tables - -```go -rt, _ := aws.VPC.CreateRouteTable(ctx, netdriver.RouteTableConfig{ - VPCID: vpc.ID, -}) -aws.VPC.AssociateRouteTable(ctx, rt.ID, subnet.ID) -``` - -### Flow Logs - -```go -aws.VPC.CreateFlowLog(ctx, netdriver.FlowLogConfig{ - ResourceID: vpc.ID, TrafficType: "ALL", -}) -``` diff --git a/frontend/content/docs/services/notification.mdx b/frontend/content/docs/services/notification.mdx deleted file mode 100644 index e30a6a64..00000000 --- a/frontend/content/docs/services/notification.mdx +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Notification -description: Topics, subscriptions, and push notifications ---- - -# Notification - -Emulates notification services: **SNS** (AWS), **NotificationHubs** (Azure), **FCM** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | SNS | `aws.SNS` | -| Azure | Notification Hubs | `azure.NotificationHubs` | -| GCP | FCM | `gcp.FCM` | - -## Key Operations - -### Topics and Subscriptions - -```go -import notifdriver "github.com/stackshy/cloudemu/notification/driver" - -topic, _ := aws.SNS.CreateTopic(ctx, notifdriver.TopicConfig{ - Name: "order-events", -}) - -sub, _ := aws.SNS.Subscribe(ctx, notifdriver.SubscriptionConfig{ - TopicID: topic.ID, - Protocol: "email", - Endpoint: "team@example.com", -}) -``` - -### Publishing - -```go -aws.SNS.Publish(ctx, notifdriver.PublishInput{ - TopicID: topic.ID, - Message: "New order received", - Attributes: map[string]string{"orderType": "express"}, -}) -``` diff --git a/frontend/content/docs/services/secrets.mdx b/frontend/content/docs/services/secrets.mdx deleted file mode 100644 index c786cd96..00000000 --- a/frontend/content/docs/services/secrets.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Secrets -description: Secret storage and versioning ---- - -# Secrets - -Emulates secret management: **SecretsManager** (AWS), **KeyVault** (Azure), **SecretManager** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | Secrets Manager | `aws.SecretsManager` | -| Azure | Key Vault | `azure.KeyVault` | -| GCP | Secret Manager | `gcp.SecretManager` | - -## Key Operations - -### Creating Secrets - -```go -import secdriver "github.com/stackshy/cloudemu/secrets/driver" - -aws.SecretsManager.CreateSecret(ctx, secdriver.SecretConfig{ - Name: "db-password", - Value: "super-secret-123", - Tags: map[string]string{"env": "production"}, -}) -``` - -### Reading Secrets - -```go -secret, _ := aws.SecretsManager.GetSecret(ctx, "db-password") -fmt.Println(secret.Value) // "super-secret-123" -``` - -### Versioning - -```go -// Update creates a new version -aws.SecretsManager.UpdateSecret(ctx, "db-password", "new-password-456") - -// Get specific version -secret, _ = aws.SecretsManager.GetSecretVersion(ctx, "db-password", "v2") -``` - -### Listing - -```go -secrets, _ := aws.SecretsManager.ListSecrets(ctx) -``` diff --git a/frontend/content/docs/services/serverless.mdx b/frontend/content/docs/services/serverless.mdx deleted file mode 100644 index 9673de4a..00000000 --- a/frontend/content/docs/services/serverless.mdx +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: Serverless -description: Function-as-a-service with versions, aliases, and layers ---- - -# Serverless - -Emulates serverless functions: **Lambda** (AWS), **Functions** (Azure), **CloudFunctions** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | Lambda | `aws.Lambda` | -| Azure | Functions | `azure.Functions` | -| GCP | Cloud Functions | `gcp.CloudFunctions` | - -## Key Operations - -### Function Lifecycle - -```go -import sdriver "github.com/stackshy/cloudemu/serverless/driver" - -// Create a function -aws.Lambda.CreateFunction(ctx, sdriver.FunctionConfig{ - Name: "my-handler", - Runtime: "go1.x", - Handler: "main", - Memory: 128, - Timeout: 30, - Tags: map[string]string{"team": "backend"}, -}) - -// Register a Go handler -aws.Lambda.RegisterHandler(ctx, "my-handler", func(ctx context.Context, payload []byte) ([]byte, error) { - return []byte(`{"status": "ok"}`), nil -}) - -// Invoke -result, _ := aws.Lambda.InvokeFunction(ctx, "my-handler", []byte(`{"key": "value"}`)) -``` - -### Versions and Aliases - -```go -// Publish a version -version, _ := aws.Lambda.PublishVersion(ctx, "my-handler", "v1 release") - -// Create an alias pointing to a version -aws.Lambda.CreateAlias(ctx, sdriver.AliasConfig{ - FunctionName: "my-handler", Name: "prod", FunctionVersion: "1", -}) - -// Weighted routing between versions -aws.Lambda.UpdateAlias(ctx, "my-handler", "prod", sdriver.AliasUpdate{ - FunctionVersion: "2", - AdditionalVersionWeights: map[string]float64{"1": 0.1}, // 10% to v1 -}) -``` - -### Layers - -```go -layer, _ := aws.Lambda.PublishLayerVersion(ctx, sdriver.LayerConfig{ - Name: "common-libs", Description: "Shared libraries", -}) -``` - -### Concurrency - -```go -aws.Lambda.PutFunctionConcurrency(ctx, "my-handler", 100) // reserve 100 concurrent executions -conc, _ := aws.Lambda.GetFunctionConcurrency(ctx, "my-handler") -``` diff --git a/frontend/content/docs/services/storage.mdx b/frontend/content/docs/services/storage.mdx deleted file mode 100644 index 753be978..00000000 --- a/frontend/content/docs/services/storage.mdx +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: Storage -description: Object storage with buckets, versioning, and multipart upload ---- - -# Storage - -Emulates object storage: **S3** (AWS), **BlobStorage** (Azure), **GCS** (GCP). - -## Provider Mapping - -| Provider | Service | Access | -|----------|---------|--------| -| AWS | S3 | `aws.S3` | -| Azure | Blob Storage | `azure.BlobStorage` | -| GCP | GCS | `gcp.GCS` | - -## Key Operations - -### Bucket Management - -```go -aws.S3.CreateBucket(ctx, "my-bucket") - -buckets, _ := aws.S3.ListBuckets(ctx) - -aws.S3.DeleteBucket(ctx, "my-bucket") -``` - -### Object Operations - -```go -// Upload -aws.S3.PutObject(ctx, "my-bucket", "path/to/file.txt", - []byte("content"), "text/plain", map[string]string{"author": "me"}) - -// Download -obj, _ := aws.S3.GetObject(ctx, "my-bucket", "path/to/file.txt") -fmt.Println(string(obj.Data)) // "content" - -// Head (metadata only) -info, _ := aws.S3.HeadObject(ctx, "my-bucket", "path/to/file.txt") -fmt.Println(info.ContentType) // "text/plain" - -// Delete -aws.S3.DeleteObject(ctx, "my-bucket", "path/to/file.txt") - -// Copy -aws.S3.CopyObject(ctx, "dst-bucket", "copy.txt", driver.CopySource{ - Bucket: "my-bucket", Key: "path/to/file.txt", -}) -``` - -### List with Prefix and Delimiter - -```go -// List all objects with a prefix -result, _ := aws.S3.ListObjects(ctx, "my-bucket", driver.ListOptions{ - Prefix: "v1.0/", -}) - -// Folder-like listing with delimiter -result, _ = aws.S3.ListObjects(ctx, "my-bucket", driver.ListOptions{ - Delimiter: "/", -}) -// result.CommonPrefixes contains folder-like prefixes -``` - -### Multipart Upload - -```go -upload, _ := aws.S3.CreateMultipartUpload(ctx, "bucket", "large-file.bin", "application/octet-stream") - -part1, _ := aws.S3.UploadPart(ctx, "bucket", "large-file.bin", upload.UploadID, 1, data1) -part2, _ := aws.S3.UploadPart(ctx, "bucket", "large-file.bin", upload.UploadID, 2, data2) - -aws.S3.CompleteMultipartUpload(ctx, "bucket", "large-file.bin", upload.UploadID, - []driver.UploadPart{*part1, *part2}) -``` - -### Versioning - -```go -aws.S3.SetBucketVersioning(ctx, "my-bucket", true) - -enabled, _ := aws.S3.GetBucketVersioning(ctx, "my-bucket") -``` - -### Presigned URLs - -```go -url, _ := aws.S3.GeneratePresignedURL(ctx, driver.PresignedURLRequest{ - Bucket: "my-bucket", Key: "file.txt", Method: "GET", - Expiry: 15 * time.Minute, -}) -``` - -### Lifecycle Policies - -```go -aws.S3.PutLifecycleConfig(ctx, "my-bucket", driver.LifecycleConfig{ - Rules: []driver.LifecycleRule{ - {Prefix: "logs/", ExpirationDays: 30, Enabled: true}, - }, -}) - -expired, _ := aws.S3.EvaluateLifecycle(ctx, "my-bucket") -``` diff --git a/frontend/lib/services.ts b/frontend/lib/services.ts deleted file mode 100644 index 97106e1b..00000000 --- a/frontend/lib/services.ts +++ /dev/null @@ -1,28 +0,0 @@ -export interface ServiceMapping { - category: string; - icon: string; - aws: string; - azure: string; - gcp: string; - slug: string; - description: string; -} - -export const services: ServiceMapping[] = [ - { category: 'Compute', icon: 'Server', aws: 'EC2', azure: 'VirtualMachines', gcp: 'GCE', slug: 'compute', description: 'Virtual machine instances with lifecycle state machines' }, - { category: 'Storage', icon: 'HardDrive', aws: 'S3', azure: 'BlobStorage', gcp: 'GCS', slug: 'storage', description: 'Object storage with buckets, versioning, and multipart upload' }, - { category: 'Database', icon: 'Database', aws: 'DynamoDB', azure: 'CosmosDB', gcp: 'Firestore', slug: 'database', description: 'NoSQL database with queries, TTL, and streams' }, - { category: 'Serverless', icon: 'Zap', aws: 'Lambda', azure: 'Functions', gcp: 'CloudFunctions', slug: 'serverless', description: 'Function-as-a-service with versions and aliases' }, - { category: 'Networking', icon: 'Network', aws: 'VPC', azure: 'VNet', gcp: 'VPC', slug: 'networking', description: 'Virtual networks, subnets, and security groups' }, - { category: 'Monitoring', icon: 'Activity', aws: 'CloudWatch', azure: 'Monitor', gcp: 'CloudMonitoring', slug: 'monitoring', description: 'Metrics, alarms, and metric queries' }, - { category: 'IAM', icon: 'Shield', aws: 'IAM', azure: 'IAM', gcp: 'IAM', slug: 'iam', description: 'Identity, roles, and policy evaluation' }, - { category: 'DNS', icon: 'Globe', aws: 'Route53', azure: 'DNS', gcp: 'CloudDNS', slug: 'dns', description: 'DNS zones and records with weighted routing' }, - { category: 'Load Balancer', icon: 'GitBranch', aws: 'ELB', azure: 'LB', gcp: 'LB', slug: 'loadbalancer', description: 'Load balancers, target groups, and health checks' }, - { category: 'Message Queue', icon: 'MessageSquare', aws: 'SQS', azure: 'ServiceBus', gcp: 'PubSub', slug: 'messagequeue', description: 'Queues with FIFO dedup and dead-letter queues' }, - { category: 'Notification', icon: 'Bell', aws: 'SNS', azure: 'NotificationHubs', gcp: 'FCM', slug: 'notification', description: 'Topics, subscriptions, and push notifications' }, - { category: 'Event Bus', icon: 'Radio', aws: 'EventBridge', azure: 'EventGrid', gcp: 'Eventarc', slug: 'eventbus', description: 'Event routing with rules and targets' }, - { category: 'Container Registry', icon: 'Box', aws: 'ECR', azure: 'ACR', gcp: 'ArtifactRegistry', slug: 'containerregistry', description: 'Container image storage and lifecycle' }, - { category: 'Cache', icon: 'MemoryStick', aws: 'ElastiCache', azure: 'Cache', gcp: 'Memorystore', slug: 'cache', description: 'In-memory cache with TTL support' }, - { category: 'Secrets', icon: 'Lock', aws: 'SecretsManager', azure: 'KeyVault', gcp: 'SecretManager', slug: 'secrets', description: 'Secret storage and versioning' }, - { category: 'Logging', icon: 'FileText', aws: 'CloudWatchLogs', azure: 'LogAnalytics', gcp: 'CloudLogging', slug: 'logging', description: 'Log groups and log streams' }, -]; diff --git a/frontend/lib/source.ts b/frontend/lib/source.ts deleted file mode 100644 index e935d703..00000000 --- a/frontend/lib/source.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { docs, meta } from '@/.source/server'; -import { loader } from 'fumadocs-core/source'; -import { toFumadocsSource } from 'fumadocs-mdx/runtime/server'; - -export const source = loader({ - baseUrl: '/docs', - source: toFumadocsSource(docs, meta) as any, -}); diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs deleted file mode 100644 index 457dcf29..00000000 --- a/frontend/next.config.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import { createMDX } from 'fumadocs-mdx/next'; - -const withMDX = createMDX(); - -/** @type {import('next').NextConfig} */ -const config = { - reactStrictMode: true, -}; - -export default withMDX(config); diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 122365ed..00000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,6248 +0,0 @@ -{ - "name": "frontend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@tailwindcss/postcss": "^4.2.2", - "autoprefixer": "^10.4.27", - "framer-motion": "^12.38.0", - "fumadocs-core": "^16.7.6", - "fumadocs-mdx": "^14.2.11", - "fumadocs-ui": "^16.7.6", - "lucide-react": "^1.7.0", - "next": "^16.2.1", - "next-themes": "^0.4.6", - "postcss": "^8.5.8", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "tailwindcss": "^4.2.2" - }, - "devDependencies": { - "@types/node": "^25.5.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "typescript": "^6.0.2" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, - "node_modules/@formatjs/fast-memoize": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.1.tgz", - "integrity": "sha512-CbNbf+tlJn1baRnPkNePnBqTLxGliG6DDgNa/UtV66abwIjwsliPMOt0172tzxABYzSuxZBZfcp//qI8AvBWPg==", - "license": "MIT" - }, - "node_modules/@formatjs/intl-localematcher": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.2.tgz", - "integrity": "sha512-q05KMYGJLyqFNFtIb8NhWLF5X3aK/k0wYt7dnRFuy6aLQL+vUwQ1cg5cO4qawEiINybeCPXAWlprY2mSBjSXAQ==", - "license": "MIT", - "dependencies": { - "@formatjs/fast-memoize": "3.1.1" - } - }, - "node_modules/@fumadocs/tailwind": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@fumadocs/tailwind/-/tailwind-0.0.3.tgz", - "integrity": "sha512-/FWcggMz9BhoX+13xBoZLX+XX9mYvJ50dkTqy3IfocJqua65ExcsKfxwKH8hgTO3vA5KnWv4+4jU7LaW2AjAmQ==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^7.1.1" - }, - "peerDependencies": { - "tailwindcss": "^4.0.0" - }, - "peerDependenciesMeta": { - "tailwindcss": { - "optional": true - } - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mdx-js/mdx": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", - "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdx": "^2.0.0", - "acorn": "^8.0.0", - "collapse-white-space": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-util-scope": "^1.0.0", - "estree-walker": "^3.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "markdown-extensions": "^2.0.0", - "recma-build-jsx": "^1.0.0", - "recma-jsx": "^1.0.0", - "recma-stringify": "^1.0.0", - "rehype-recma": "^1.0.0", - "remark-mdx": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "source-map": "^0.7.0", - "unified": "^11.0.0", - "unist-util-position-from-estree": "^2.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@next/env": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz", - "integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==", - "license": "MIT" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz", - "integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", - "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", - "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", - "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", - "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", - "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", - "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", - "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@orama/orama": { - "version": "3.1.18", - "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-3.1.18.tgz", - "integrity": "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==", - "license": "Apache-2.0", - "engines": { - "node": ">= 20.0.0" - } - }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-accordion": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", - "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", - "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", - "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", - "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@shikijs/core": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", - "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", - "license": "MIT", - "dependencies": { - "@shikijs/primitive": "4.0.2", - "@shikijs/types": "4.0.2", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.5" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/engine-javascript": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", - "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.0.2", - "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.4" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", - "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.0.2", - "@shikijs/vscode-textmate": "^10.0.2" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/langs": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", - "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.0.2" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/primitive": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", - "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.0.2", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/rehype": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/rehype/-/rehype-4.0.2.tgz", - "integrity": "sha512-cmPlKLD8JeojasNFoY64162ScpEdEdQUMuVodPCrv1nx1z3bjmGwoKWDruQWa/ejSznImlaeB0Ty6Q3zPaVQAA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.0.2", - "@types/hast": "^3.0.4", - "hast-util-to-string": "^3.0.1", - "shiki": "4.0.2", - "unified": "^11.0.5", - "unist-util-visit": "^5.1.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/themes": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", - "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.0.2" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/transformers": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-4.0.2.tgz", - "integrity": "sha512-1+L0gf9v+SdDXs08vjaLb3mBFa8U7u37cwcBQIv/HCocLwX69Tt6LpUCjtB+UUTvQxI7BnjZKhN/wMjhHBcJGg==", - "license": "MIT", - "dependencies": { - "@shikijs/core": "4.0.2", - "@shikijs/types": "4.0.2" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", - "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", - "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", - "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.2.2" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", - "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-x64": "4.2.2", - "@tailwindcss/oxide-freebsd-x64": "4.2.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-x64-musl": "4.2.2", - "@tailwindcss/oxide-wasm32-wasi": "4.2.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", - "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", - "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", - "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", - "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", - "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", - "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", - "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", - "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", - "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", - "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", - "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", - "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", - "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.2.2", - "@tailwindcss/oxide": "4.2.2", - "postcss": "^8.5.6", - "tailwindcss": "4.2.2" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mdx": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", - "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/astring": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", - "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", - "license": "MIT", - "bin": { - "astring": "bin/astring" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.27", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", - "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001774", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.10", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", - "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001781", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", - "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/collapse-white-space": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", - "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/compute-scroll-into-view": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", - "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.325", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz", - "integrity": "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==", - "license": "ISC" - }, - "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/esast-util-from-estree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", - "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esast-util-from-js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", - "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "acorn": "^8.0.0", - "esast-util-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/estree-util-attach-comments": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", - "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-build-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", - "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-walker": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-scope": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", - "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-to-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", - "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "astring": "^1.8.0", - "source-map": "^0.7.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-value-to-estree": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", - "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/remcohaszing" - } - }, - "node_modules/estree-util-visit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", - "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/framer-motion": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", - "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", - "license": "MIT", - "dependencies": { - "motion-dom": "^12.38.0", - "motion-utils": "^12.36.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/fumadocs-core": { - "version": "16.7.6", - "resolved": "https://registry.npmjs.org/fumadocs-core/-/fumadocs-core-16.7.6.tgz", - "integrity": "sha512-d4HtGupFpcSWQqLbWh184yoEg6D70pH68NP77Ct4mI0N61t/Uy63wYj9sbS1h/m6jlijUIXC6rz8D5JApOB9Wg==", - "license": "MIT", - "dependencies": { - "@formatjs/intl-localematcher": "^0.8.2", - "@orama/orama": "^3.1.18", - "@shikijs/rehype": "^4.0.2", - "@shikijs/transformers": "^4.0.2", - "estree-util-value-to-estree": "^3.5.0", - "github-slugger": "^2.0.0", - "hast-util-to-estree": "^3.1.3", - "hast-util-to-jsx-runtime": "^2.3.6", - "image-size": "^2.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-markdown": "^2.1.2", - "negotiator": "^1.0.0", - "npm-to-yarn": "^3.0.1", - "path-to-regexp": "^8.3.0", - "remark": "^15.0.1", - "remark-gfm": "^4.0.1", - "remark-rehype": "^11.1.2", - "scroll-into-view-if-needed": "^3.1.0", - "shiki": "^4.0.2", - "tinyglobby": "^0.2.15", - "unified": "^11.0.5", - "unist-util-visit": "^5.1.0", - "vfile": "^6.0.3" - }, - "peerDependencies": { - "@mdx-js/mdx": "*", - "@mixedbread/sdk": "^0.46.0", - "@orama/core": "1.x.x", - "@oramacloud/client": "2.x.x", - "@tanstack/react-router": "1.x.x", - "@types/estree-jsx": "*", - "@types/hast": "*", - "@types/mdast": "*", - "@types/react": "*", - "algoliasearch": "5.x.x", - "flexsearch": "*", - "lucide-react": "*", - "next": "16.x.x", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-router": "7.x.x", - "waku": "^0.26.0 || ^0.27.0 || ^1.0.0", - "zod": "4.x.x" - }, - "peerDependenciesMeta": { - "@mdx-js/mdx": { - "optional": true - }, - "@mixedbread/sdk": { - "optional": true - }, - "@orama/core": { - "optional": true - }, - "@oramacloud/client": { - "optional": true - }, - "@tanstack/react-router": { - "optional": true - }, - "@types/estree-jsx": { - "optional": true - }, - "@types/hast": { - "optional": true - }, - "@types/mdast": { - "optional": true - }, - "@types/react": { - "optional": true - }, - "algoliasearch": { - "optional": true - }, - "flexsearch": { - "optional": true - }, - "lucide-react": { - "optional": true - }, - "next": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-router": { - "optional": true - }, - "waku": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/fumadocs-mdx": { - "version": "14.2.11", - "resolved": "https://registry.npmjs.org/fumadocs-mdx/-/fumadocs-mdx-14.2.11.tgz", - "integrity": "sha512-j0gHKs45c62ARteE8/yBM2Nu2I8AE2Cs37ktPEdc/8EX7TL66XP74un5OpHp6itLyWTu8Jur0imOiiIDq8+rDg==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.1.1", - "@standard-schema/spec": "^1.1.0", - "chokidar": "^5.0.0", - "esbuild": "^0.27.3", - "estree-util-value-to-estree": "^3.5.0", - "js-yaml": "^4.1.1", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-markdown": "^2.1.2", - "picocolors": "^1.1.1", - "picomatch": "^4.0.3", - "tinyexec": "^1.0.4", - "tinyglobby": "^0.2.15", - "unified": "^11.0.5", - "unist-util-remove-position": "^5.0.0", - "unist-util-visit": "^5.1.0", - "vfile": "^6.0.3", - "zod": "^4.3.6" - }, - "bin": { - "fumadocs-mdx": "dist/bin.js" - }, - "peerDependencies": { - "@fumadocs/mdx-remote": "^1.4.0", - "@types/mdast": "*", - "@types/mdx": "*", - "@types/react": "*", - "fumadocs-core": "^15.0.0 || ^16.0.0", - "mdast-util-directive": "*", - "next": "^15.3.0 || ^16.0.0", - "react": "*", - "vite": "6.x.x || 7.x.x || 8.x.x" - }, - "peerDependenciesMeta": { - "@fumadocs/mdx-remote": { - "optional": true - }, - "@types/mdast": { - "optional": true - }, - "@types/mdx": { - "optional": true - }, - "@types/react": { - "optional": true - }, - "mdast-util-directive": { - "optional": true - }, - "next": { - "optional": true - }, - "react": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/fumadocs-ui": { - "version": "16.7.6", - "resolved": "https://registry.npmjs.org/fumadocs-ui/-/fumadocs-ui-16.7.6.tgz", - "integrity": "sha512-wjZnm8SiX2lj5zWOlOHnzSZ0YBFwNqYGBX1u5F3mZtdIkmkDVs+3+JngCkRHNZzYJVBulXjp8t5wzBz0yDJa8w==", - "license": "MIT", - "dependencies": { - "@fumadocs/tailwind": "0.0.3", - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-direction": "^1.1.1", - "@radix-ui/react-navigation-menu": "^1.2.14", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-presence": "^1.1.5", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tabs": "^1.1.13", - "class-variance-authority": "^0.7.1", - "lucide-react": "^1.6.0", - "motion": "^12.38.0", - "next-themes": "^0.4.6", - "react-medium-image-zoom": "^5.4.1", - "react-remove-scroll": "^2.7.2", - "rehype-raw": "^7.0.0", - "scroll-into-view-if-needed": "^3.1.0", - "tailwind-merge": "^3.5.0", - "unist-util-visit": "^5.1.0" - }, - "peerDependencies": { - "@takumi-rs/image-response": "*", - "@types/mdx": "*", - "@types/react": "*", - "fumadocs-core": "16.7.6", - "next": "16.x.x", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "shiki": "*" - }, - "peerDependenciesMeta": { - "@takumi-rs/image-response": { - "optional": true - }, - "@types/mdx": { - "optional": true - }, - "@types/react": { - "optional": true - }, - "next": { - "optional": true - }, - "shiki": { - "optional": true - } - } - }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/github-slugger": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", - "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", - "license": "ISC" - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", - "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^9.0.0", - "property-information": "^7.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-raw": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", - "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-from-parse5": "^8.0.0", - "hast-util-to-parse5": "^8.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "parse5": "^7.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-estree": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", - "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-attach-comments": "^3.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-html": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^3.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.0", - "zwitch": "^2.0.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", - "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-string": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", - "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/image-size": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", - "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", - "license": "MIT", - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/inline-style-parser": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", - "license": "MIT" - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "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" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lucide-react": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", - "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/markdown-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", - "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", - "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-expression": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", - "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-jsx": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", - "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-md": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", - "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", - "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", - "license": "MIT", - "dependencies": { - "acorn": "^8.0.0", - "acorn-jsx": "^5.0.0", - "micromark-extension-mdx-expression": "^3.0.0", - "micromark-extension-mdx-jsx": "^3.0.0", - "micromark-extension-mdx-md": "^2.0.0", - "micromark-extension-mdxjs-esm": "^3.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", - "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-mdx-expression": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", - "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-events-to-acorn": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", - "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/motion": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz", - "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==", - "license": "MIT", - "dependencies": { - "framer-motion": "^12.38.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/motion-dom": { - "version": "12.38.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", - "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", - "license": "MIT", - "dependencies": { - "motion-utils": "^12.36.0" - } - }, - "node_modules/motion-utils": { - "version": "12.36.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", - "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/next": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", - "integrity": "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==", - "license": "MIT", - "dependencies": { - "@next/env": "16.2.1", - "@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" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=20.9.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.1", - "@next/swc-darwin-x64": "16.2.1", - "@next/swc-linux-arm64-gnu": "16.2.1", - "@next/swc-linux-arm64-musl": "16.2.1", - "@next/swc-linux-x64-gnu": "16.2.1", - "@next/swc-linux-x64-musl": "16.2.1", - "@next/swc-win32-arm64-msvc": "16.2.1", - "@next/swc-win32-x64-msvc": "16.2.1", - "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" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/next-themes": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", - "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" - } - }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "license": "MIT" - }, - "node_modules/npm-to-yarn": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-to-yarn/-/npm-to-yarn-3.0.1.tgz", - "integrity": "sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A==", - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/nebrelbug/npm-to-yarn?sponsor=1" - } - }, - "node_modules/oniguruma-parser": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", - "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", - "license": "MIT" - }, - "node_modules/oniguruma-to-es": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", - "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", - "license": "MIT", - "dependencies": { - "oniguruma-parser": "^0.12.1", - "regex": "^6.1.0", - "regex-recursion": "^6.0.2" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, - "node_modules/react-medium-image-zoom": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/react-medium-image-zoom/-/react-medium-image-zoom-5.4.1.tgz", - "integrity": "sha512-DD2iZYaCfAwiQGR8AN62r/cDJYoXhezlYJc5HY4TzBUGuGge43CptG0f7m0PEIM72aN6GfpjohvY1yYdtCJB7g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/rpearce" - } - ], - "license": "BSD-3-Clause", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/recma-build-jsx": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", - "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-util-build-jsx": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/recma-jsx": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", - "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", - "license": "MIT", - "dependencies": { - "acorn-jsx": "^5.0.0", - "estree-util-to-js": "^2.0.0", - "recma-parse": "^1.0.0", - "recma-stringify": "^1.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/recma-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", - "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "esast-util-from-js": "^2.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/recma-stringify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", - "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-util-to-js": "^2.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", - "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", - "license": "MIT", - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-recursion": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", - "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", - "license": "MIT", - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-utilities": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", - "license": "MIT" - }, - "node_modules/rehype-raw": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", - "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-raw": "^9.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-recma": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", - "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "hast-util-to-estree": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", - "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", - "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", - "license": "MIT", - "dependencies": { - "mdast-util-mdx": "^3.0.0", - "micromark-extension-mdxjs": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/scroll-into-view-if-needed": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", - "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", - "license": "MIT", - "dependencies": { - "compute-scroll-into-view": "^3.0.2" - } - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/shiki": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", - "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", - "license": "MIT", - "dependencies": { - "@shikijs/core": "4.0.2", - "@shikijs/engine-javascript": "4.0.2", - "@shikijs/engine-oniguruma": "4.0.2", - "@shikijs/langs": "4.0.2", - "@shikijs/themes": "4.0.2", - "@shikijs/types": "4.0.2", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/style-to-js": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", - "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.14" - } - }, - "node_modules/style-to-object": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.7" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/tailwind-merge": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", - "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", - "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", - "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position-from-estree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", - "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", - "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", - "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 8eaccf0c..00000000 --- a/frontend/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "cloudemu-docs", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@tailwindcss/postcss": "^4.2.2", - "autoprefixer": "^10.4.27", - "framer-motion": "^12.38.0", - "fumadocs-core": "^16.7.6", - "fumadocs-mdx": "^14.2.11", - "fumadocs-ui": "^16.7.6", - "lucide-react": "^1.7.0", - "next": "^16.2.1", - "next-themes": "^0.4.6", - "postcss": "^8.5.8", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "tailwindcss": "^4.2.2" - }, - "devDependencies": { - "@types/node": "^25.5.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "typescript": "^6.0.2" - } -} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs deleted file mode 100644 index 5d6d8457..00000000 --- a/frontend/postcss.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - '@tailwindcss/postcss': {}, - }, -}; - -export default config; diff --git a/frontend/source.config.ts b/frontend/source.config.ts deleted file mode 100644 index 8dc21074..00000000 --- a/frontend/source.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineDocs, defineConfig } from 'fumadocs-mdx/config'; - -export const { docs, meta } = defineDocs({ - dir: 'content/docs', -}); - -export default defineConfig(); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index 7796a883..00000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2017", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "react-jsx", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": [ - "./*" - ] - } - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "**/*.mdx", - ".source/**/*.ts", - ".next/types/**/*.ts", - ".next/dev/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] -} From 6895042c1e813be4352732a26300933c65b7b6a0 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sat, 28 Mar 2026 18:39:37 +0530 Subject: [PATCH 3/8] add UpdateItem to database service for partial attribute updates Closes #58 --- cloudemu_test.go | 398 +++++++++++++++++++++++++++ database/database.go | 16 ++ database/driver/driver.go | 15 + providers/aws/dynamodb/dynamodb.go | 44 +++ providers/azure/cosmosdb/cosmosdb.go | 42 +++ providers/gcp/firestore/firestore.go | 42 +++ 6 files changed, 557 insertions(+) diff --git a/cloudemu_test.go b/cloudemu_test.go index 31c0efef..69233a3a 100644 --- a/cloudemu_test.go +++ b/cloudemu_test.go @@ -5570,3 +5570,401 @@ func TestGCPMetricsEmission(t *testing.T) { } }) } + +func TestUpdateItemAWS(t *testing.T) { + ctx := context.Background() + p := NewAWS() + + if err := p.DynamoDB.CreateTable(ctx, driver.TableConfig{ + Name: "users", PartitionKey: "pk", SortKey: "sk", + }); err != nil { + t.Fatal(err) + } + + // Put initial item + if err := p.DynamoDB.PutItem(ctx, "users", map[string]any{ + "pk": "user1", "sk": "profile", "name": "Alice", "age": 30, "city": "NYC", + }); err != nil { + t.Fatal(err) + } + + // SET: update name and add new field + updated, err := p.DynamoDB.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "user1", "sk": "profile"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "name", Value: "Alice Smith"}, + {Action: "SET", Field: "email", Value: "alice@example.com"}, + }, + }) + if err != nil { + t.Fatal(err) + } + + if updated["name"] != "Alice Smith" { + t.Errorf("expected 'Alice Smith', got %v", updated["name"]) + } + + if updated["email"] != "alice@example.com" { + t.Errorf("expected 'alice@example.com', got %v", updated["email"]) + } + + if updated["age"] != 30 { + t.Errorf("expected age 30 preserved, got %v", updated["age"]) + } + + // REMOVE: remove city field + updated, err = p.DynamoDB.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "user1", "sk": "profile"}, + Actions: []driver.UpdateAction{ + {Action: "REMOVE", Field: "city"}, + }, + }) + if err != nil { + t.Fatal(err) + } + + if _, hasCityField := updated["city"]; hasCityField { + t.Error("expected city field to be removed") + } + + if updated["name"] != "Alice Smith" { + t.Errorf("expected name preserved as 'Alice Smith', got %v", updated["name"]) + } + + // Verify via GetItem + got, err := p.DynamoDB.GetItem(ctx, "users", map[string]any{"pk": "user1", "sk": "profile"}) + if err != nil { + t.Fatal(err) + } + + if got["name"] != "Alice Smith" { + t.Errorf("GetItem: expected 'Alice Smith', got %v", got["name"]) + } + + if got["email"] != "alice@example.com" { + t.Errorf("GetItem: expected 'alice@example.com', got %v", got["email"]) + } + + if _, hasCityField := got["city"]; hasCityField { + t.Error("GetItem: expected city field to be removed") + } +} + +func TestUpdateItemAzure(t *testing.T) { + ctx := context.Background() + p := NewAzure() + + if err := p.CosmosDB.CreateTable(ctx, driver.TableConfig{ + Name: "users", PartitionKey: "pk", SortKey: "sk", + }); err != nil { + t.Fatal(err) + } + + if err := p.CosmosDB.PutItem(ctx, "users", map[string]any{ + "pk": "user1", "sk": "profile", "name": "Alice", "age": 30, "city": "NYC", + }); err != nil { + t.Fatal(err) + } + + // SET fields + updated, err := p.CosmosDB.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "user1", "sk": "profile"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "name", Value: "Alice Smith"}, + {Action: "SET", Field: "email", Value: "alice@example.com"}, + }, + }) + if err != nil { + t.Fatal(err) + } + + if updated["name"] != "Alice Smith" { + t.Errorf("expected 'Alice Smith', got %v", updated["name"]) + } + + if updated["email"] != "alice@example.com" { + t.Errorf("expected 'alice@example.com', got %v", updated["email"]) + } + + if updated["age"] != 30 { + t.Errorf("expected age 30 preserved, got %v", updated["age"]) + } + + // REMOVE field + updated, err = p.CosmosDB.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "user1", "sk": "profile"}, + Actions: []driver.UpdateAction{ + {Action: "REMOVE", Field: "city"}, + }, + }) + if err != nil { + t.Fatal(err) + } + + if _, hasCityField := updated["city"]; hasCityField { + t.Error("expected city field to be removed") + } + + // Verify via GetItem + got, err := p.CosmosDB.GetItem(ctx, "users", map[string]any{"pk": "user1", "sk": "profile"}) + if err != nil { + t.Fatal(err) + } + + if got["name"] != "Alice Smith" { + t.Errorf("GetItem: expected 'Alice Smith', got %v", got["name"]) + } +} + +func TestUpdateItemGCP(t *testing.T) { + ctx := context.Background() + p := NewGCP() + + if err := p.Firestore.CreateTable(ctx, driver.TableConfig{ + Name: "users", PartitionKey: "pk", SortKey: "sk", + }); err != nil { + t.Fatal(err) + } + + if err := p.Firestore.PutItem(ctx, "users", map[string]any{ + "pk": "user1", "sk": "profile", "name": "Alice", "age": 30, "city": "NYC", + }); err != nil { + t.Fatal(err) + } + + // SET fields + updated, err := p.Firestore.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "user1", "sk": "profile"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "name", Value: "Alice Smith"}, + {Action: "SET", Field: "email", Value: "alice@example.com"}, + }, + }) + if err != nil { + t.Fatal(err) + } + + if updated["name"] != "Alice Smith" { + t.Errorf("expected 'Alice Smith', got %v", updated["name"]) + } + + if updated["email"] != "alice@example.com" { + t.Errorf("expected 'alice@example.com', got %v", updated["email"]) + } + + if updated["age"] != 30 { + t.Errorf("expected age 30 preserved, got %v", updated["age"]) + } + + // REMOVE field + updated, err = p.Firestore.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "user1", "sk": "profile"}, + Actions: []driver.UpdateAction{ + {Action: "REMOVE", Field: "city"}, + }, + }) + if err != nil { + t.Fatal(err) + } + + if _, hasCityField := updated["city"]; hasCityField { + t.Error("expected city field to be removed") + } + + // Verify via GetItem + got, err := p.Firestore.GetItem(ctx, "users", map[string]any{"pk": "user1", "sk": "profile"}) + if err != nil { + t.Fatal(err) + } + + if got["name"] != "Alice Smith" { + t.Errorf("GetItem: expected 'Alice Smith', got %v", got["name"]) + } +} + +func TestUpdateItemNotFound(t *testing.T) { + ctx := context.Background() + + t.Run("AWS", func(t *testing.T) { + p := NewAWS() + + if err := p.DynamoDB.CreateTable(ctx, driver.TableConfig{ + Name: "t1", PartitionKey: "pk", + }); err != nil { + t.Fatal(err) + } + + _, err := p.DynamoDB.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "t1", + Key: map[string]any{"pk": "missing"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "x", Value: 1}}, + }) + if !cerrors.IsNotFound(err) { + t.Errorf("expected NotFound, got %v", err) + } + }) + + t.Run("Azure", func(t *testing.T) { + p := NewAzure() + + if err := p.CosmosDB.CreateTable(ctx, driver.TableConfig{ + Name: "t1", PartitionKey: "pk", + }); err != nil { + t.Fatal(err) + } + + _, err := p.CosmosDB.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "t1", + Key: map[string]any{"pk": "missing"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "x", Value: 1}}, + }) + if !cerrors.IsNotFound(err) { + t.Errorf("expected NotFound, got %v", err) + } + }) + + t.Run("GCP", func(t *testing.T) { + p := NewGCP() + + if err := p.Firestore.CreateTable(ctx, driver.TableConfig{ + Name: "t1", PartitionKey: "pk", + }); err != nil { + t.Fatal(err) + } + + _, err := p.Firestore.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "t1", + Key: map[string]any{"pk": "missing"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "x", Value: 1}}, + }) + if !cerrors.IsNotFound(err) { + t.Errorf("expected NotFound, got %v", err) + } + }) +} + +func TestUpdateItemTableNotFound(t *testing.T) { + ctx := context.Background() + + t.Run("AWS", func(t *testing.T) { + p := NewAWS() + + _, err := p.DynamoDB.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "nonexistent", + Key: map[string]any{"pk": "x"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "x", Value: 1}}, + }) + if !cerrors.IsNotFound(err) { + t.Errorf("expected NotFound, got %v", err) + } + }) + + t.Run("Azure", func(t *testing.T) { + p := NewAzure() + + _, err := p.CosmosDB.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "nonexistent", + Key: map[string]any{"pk": "x"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "x", Value: 1}}, + }) + if !cerrors.IsNotFound(err) { + t.Errorf("expected NotFound, got %v", err) + } + }) + + t.Run("GCP", func(t *testing.T) { + p := NewGCP() + + _, err := p.Firestore.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "nonexistent", + Key: map[string]any{"pk": "x"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "x", Value: 1}}, + }) + if !cerrors.IsNotFound(err) { + t.Errorf("expected NotFound, got %v", err) + } + }) +} + +func TestUpdateItemInvalidAction(t *testing.T) { + ctx := context.Background() + p := NewAWS() + + if err := p.DynamoDB.CreateTable(ctx, driver.TableConfig{ + Name: "t1", PartitionKey: "pk", + }); err != nil { + t.Fatal(err) + } + + if err := p.DynamoDB.PutItem(ctx, "t1", map[string]any{"pk": "k1", "v": 1}); err != nil { + t.Fatal(err) + } + + _, err := p.DynamoDB.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "t1", + Key: map[string]any{"pk": "k1"}, + Actions: []driver.UpdateAction{{Action: "INVALID", Field: "v", Value: 2}}, + }) + if err == nil { + t.Error("expected error for invalid action, got nil") + } +} + +func TestUpdateItemWithStreams(t *testing.T) { + ctx := context.Background() + p := NewAWS() + + if err := p.DynamoDB.CreateTable(ctx, driver.TableConfig{ + Name: "t1", PartitionKey: "pk", + }); err != nil { + t.Fatal(err) + } + + if err := p.DynamoDB.UpdateStreamConfig(ctx, "t1", driver.StreamConfig{ + Enabled: true, ViewType: "NEW_AND_OLD_IMAGES", + }); err != nil { + t.Fatal(err) + } + + if err := p.DynamoDB.PutItem(ctx, "t1", map[string]any{"pk": "k1", "val": "old"}); err != nil { + t.Fatal(err) + } + + _, err := p.DynamoDB.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "t1", + Key: map[string]any{"pk": "k1"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "val", Value: "new"}}, + }) + if err != nil { + t.Fatal(err) + } + + iter, err := p.DynamoDB.GetStreamRecords(ctx, "t1", 10, "") + if err != nil { + t.Fatal(err) + } + + // Should have INSERT (from PutItem) + MODIFY (from UpdateItem) + if len(iter.Records) != 2 { + t.Fatalf("expected 2 stream records, got %d", len(iter.Records)) + } + + modifyRec := iter.Records[1] + if modifyRec.EventType != "MODIFY" { + t.Errorf("expected MODIFY event, got %s", modifyRec.EventType) + } + + if modifyRec.OldImage["val"] != "old" { + t.Errorf("expected old image val='old', got %v", modifyRec.OldImage["val"]) + } + + if modifyRec.NewImage["val"] != "new" { + t.Errorf("expected new image val='new', got %v", modifyRec.NewImage["val"]) + } +} diff --git a/database/database.go b/database/database.go index de67ba24..40ccdf4f 100644 --- a/database/database.go +++ b/database/database.go @@ -135,6 +135,22 @@ func (db *Database) GetItem(ctx context.Context, table string, key map[string]an return out.(map[string]any), nil } +func (db *Database) UpdateItem(ctx context.Context, input driver.UpdateItemInput) (map[string]any, error) { + out, err := db.do(ctx, "UpdateItem", map[string]any{"table": input.Table}, func() (any, error) { + return db.driver.UpdateItem(ctx, input) + }) + + if err != nil { + return nil, err + } + + if out == nil { + return nil, nil + } + + return out.(map[string]any), nil +} + func (db *Database) DeleteItem(ctx context.Context, table string, key map[string]any) error { _, err := db.do(ctx, "DeleteItem", map[string]any{"table": table}, func() (any, error) { return nil, db.driver.DeleteItem(ctx, table, key) diff --git a/database/driver/driver.go b/database/driver/driver.go index c0028bf3..b7dc29d3 100644 --- a/database/driver/driver.go +++ b/database/driver/driver.go @@ -52,6 +52,20 @@ type GSIConfig struct { SortKey string } +// UpdateAction represents a single field-level update action. +type UpdateAction struct { + Action string // "SET" or "REMOVE" + Field string + Value any // ignored for REMOVE +} + +// UpdateItemInput describes an update operation on an existing item. +type UpdateItemInput struct { + Table string + Key map[string]any + Actions []UpdateAction +} + // KeyCondition defines a key condition for queries. type KeyCondition struct { PartitionKey string @@ -102,6 +116,7 @@ type Database interface { PutItem(ctx context.Context, table string, item map[string]any) error GetItem(ctx context.Context, table string, key map[string]any) (map[string]any, error) + UpdateItem(ctx context.Context, input UpdateItemInput) (map[string]any, error) DeleteItem(ctx context.Context, table string, key map[string]any) error Query(ctx context.Context, input QueryInput) (*QueryResult, error) Scan(ctx context.Context, input ScanInput) (*QueryResult, error) diff --git a/providers/aws/dynamodb/dynamodb.go b/providers/aws/dynamodb/dynamodb.go index 65c64f3a..8f8dc232 100644 --- a/providers/aws/dynamodb/dynamodb.go +++ b/providers/aws/dynamodb/dynamodb.go @@ -190,6 +190,50 @@ func (m *Mock) GetItem(_ context.Context, table string, key map[string]any) (map return item, nil } +// UpdateItem applies partial updates to an existing item. +func (m *Mock) UpdateItem(_ context.Context, input driver.UpdateItemInput) (map[string]any, error) { + m.mu.Lock() + + td, exists := m.tables[input.Table] + if !exists { + m.mu.Unlock() + return nil, cerrors.Newf(cerrors.NotFound, "table %s not found", input.Table) + } + + k := itemKey(td.config, input.Key) + item, ok := td.items.Get(k) + + if !ok { + m.mu.Unlock() + return nil, cerrors.New(cerrors.NotFound, "item not found") + } + + oldItem := copyItem(item) + updated := copyItem(item) + + for _, action := range input.Actions { + switch action.Action { + case "SET": + updated[action.Field] = action.Value + case "REMOVE": + delete(updated, action.Field) + default: + m.mu.Unlock() + return nil, cerrors.Newf(cerrors.InvalidArgument, "unsupported action: %s", action.Action) + } + } + + td.items.Set(k, updated) + m.recordStreamEvent(td, oldItem, updated, true) + m.mu.Unlock() + + dims := map[string]string{"TableName": input.Table} + m.emitMetric("ConsumedWriteCapacityUnits", 1, dims) + m.emitMetric("SuccessfulRequestCount", 1, dims) + + return updated, nil +} + func (m *Mock) DeleteItem(_ context.Context, table string, key map[string]any) error { m.mu.Lock() diff --git a/providers/azure/cosmosdb/cosmosdb.go b/providers/azure/cosmosdb/cosmosdb.go index 3bb8d9c1..51699615 100644 --- a/providers/azure/cosmosdb/cosmosdb.go +++ b/providers/azure/cosmosdb/cosmosdb.go @@ -206,6 +206,48 @@ func (m *Mock) GetItem(_ context.Context, table string, key map[string]any) (map return item, nil } +// UpdateItem applies partial updates to an existing document in a container. +func (m *Mock) UpdateItem(_ context.Context, input driver.UpdateItemInput) (map[string]any, error) { + m.mu.Lock() + + td, exists := m.tables[input.Table] + if !exists { + m.mu.Unlock() + return nil, cerrors.Newf(cerrors.NotFound, "container %s not found", input.Table) + } + + k := itemKey(td.config, input.Key) + item, ok := td.items.Get(k) + + if !ok { + m.mu.Unlock() + return nil, cerrors.New(cerrors.NotFound, "item not found") + } + + oldItem := copyItem(item) + updated := copyItem(item) + + for _, action := range input.Actions { + switch action.Action { + case "SET": + updated[action.Field] = action.Value + case "REMOVE": + delete(updated, action.Field) + default: + m.mu.Unlock() + return nil, cerrors.Newf(cerrors.InvalidArgument, "unsupported action: %s", action.Action) + } + } + + td.items.Set(k, updated) + m.recordStreamEvent(td, oldItem, updated, true) + m.mu.Unlock() + + m.emitMetric(input.Table, map[string]float64{"TotalRequests": 1, "TotalRequestUnits": 1}) + + return updated, nil +} + // DeleteItem deletes an item from a container by key. func (m *Mock) DeleteItem(_ context.Context, table string, key map[string]any) error { m.mu.Lock() diff --git a/providers/gcp/firestore/firestore.go b/providers/gcp/firestore/firestore.go index ee745629..f29f64ea 100644 --- a/providers/gcp/firestore/firestore.go +++ b/providers/gcp/firestore/firestore.go @@ -194,6 +194,48 @@ func (m *Mock) GetItem(ctx context.Context, table string, key map[string]any) (m return item, nil } +// UpdateItem applies partial updates to an existing document in a collection. +func (m *Mock) UpdateItem(ctx context.Context, input driver.UpdateItemInput) (map[string]any, error) { + m.mu.Lock() + + cd, exists := m.collections[input.Table] + if !exists { + m.mu.Unlock() + return nil, cerrors.Newf(cerrors.NotFound, "collection %s not found", input.Table) + } + + k := docKey(cd.config, input.Key) + item, ok := cd.items.Get(k) + + if !ok { + m.mu.Unlock() + return nil, cerrors.New(cerrors.NotFound, "document not found") + } + + oldItem := copyItem(item) + updated := copyItem(item) + + for _, action := range input.Actions { + switch action.Action { + case "SET": + updated[action.Field] = action.Value + case "REMOVE": + delete(updated, action.Field) + default: + m.mu.Unlock() + return nil, cerrors.Newf(cerrors.InvalidArgument, "unsupported action: %s", action.Action) + } + } + + cd.items.Set(k, updated) + m.recordStreamEvent(cd, oldItem, updated, true) + m.mu.Unlock() + + m.emitMetric(ctx, "document/write_count", 1, map[string]string{"collection_id": input.Table}) + + return updated, nil +} + func (m *Mock) DeleteItem(ctx context.Context, table string, key map[string]any) error { m.mu.Lock() From 2d93d94e4be0fc2a1441ef3d0f1d7755ad1a79a2 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sat, 28 Mar 2026 18:59:32 +0530 Subject: [PATCH 4/8] add unit tests for UpdateItem in all 3 provider test files --- providers/aws/dynamodb/dynamodb_test.go | 190 ++++++++++++++++++++++ providers/azure/cosmosdb/cosmosdb_test.go | 184 +++++++++++++++++++++ providers/gcp/firestore/firestore_test.go | 189 +++++++++++++++++++++ 3 files changed, 563 insertions(+) diff --git a/providers/aws/dynamodb/dynamodb_test.go b/providers/aws/dynamodb/dynamodb_test.go index 51e5a4e2..36efbcd8 100644 --- a/providers/aws/dynamodb/dynamodb_test.go +++ b/providers/aws/dynamodb/dynamodb_test.go @@ -714,3 +714,193 @@ func assertNotEmpty(t *testing.T, s string) { t.Error("expected non-empty string") } } + +func TestUpdateItemSetFields(t *testing.T) { + m := newTestMock() + ctx := context.Background() + createTestTable(m, "tbl") + + _ = m.PutItem(ctx, "tbl", map[string]any{"pk": "u1", "sk": "info", "name": "Alice", "age": 30}) + + updated, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "tbl", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "name", Value: "Alice Smith"}, + {Action: "SET", Field: "email", Value: "alice@test.com"}, + }, + }) + requireNoError(t, err) + assertEqual(t, "Alice Smith", updated["name"]) + assertEqual(t, "alice@test.com", updated["email"]) + assertEqual(t, 30, updated["age"]) +} + +func TestUpdateItemRemoveFields(t *testing.T) { + m := newTestMock() + ctx := context.Background() + createTestTable(m, "tbl") + + _ = m.PutItem(ctx, "tbl", map[string]any{"pk": "u1", "sk": "info", "name": "Alice", "city": "NYC"}) + + updated, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "tbl", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "REMOVE", Field: "city"}, + }, + }) + requireNoError(t, err) + + if _, has := updated["city"]; has { + t.Error("expected city to be removed") + } + + assertEqual(t, "Alice", updated["name"]) +} + +func TestUpdateItemSetAndRemoveCombined(t *testing.T) { + m := newTestMock() + ctx := context.Background() + createTestTable(m, "tbl") + + _ = m.PutItem(ctx, "tbl", map[string]any{"pk": "u1", "sk": "info", "name": "Alice", "old_field": "remove_me"}) + + updated, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "tbl", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "name", Value: "Bob"}, + {Action: "REMOVE", Field: "old_field"}, + }, + }) + requireNoError(t, err) + assertEqual(t, "Bob", updated["name"]) + + if _, has := updated["old_field"]; has { + t.Error("expected old_field to be removed") + } +} + +func TestUpdateItemPersistsChanges(t *testing.T) { + m := newTestMock() + ctx := context.Background() + createTestTable(m, "tbl") + + _ = m.PutItem(ctx, "tbl", map[string]any{"pk": "u1", "sk": "info", "v": "old"}) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "tbl", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "v", Value: "new"}, + }, + }) + requireNoError(t, err) + + got, err := m.GetItem(ctx, "tbl", map[string]any{"pk": "u1", "sk": "info"}) + requireNoError(t, err) + assertEqual(t, "new", got["v"]) +} + +func TestUpdateItemTableNotFound(t *testing.T) { + m := newTestMock() + ctx := context.Background() + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "nonexistent", + Key: map[string]any{"pk": "x"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "v", Value: 1}}, + }) + assertError(t, err, true) +} + +func TestUpdateItemItemNotFound(t *testing.T) { + m := newTestMock() + ctx := context.Background() + createTestTable(m, "tbl") + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "tbl", + Key: map[string]any{"pk": "missing", "sk": "missing"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "v", Value: 1}}, + }) + assertError(t, err, true) +} + +func TestUpdateItemInvalidAction(t *testing.T) { + m := newTestMock() + ctx := context.Background() + createTestTable(m, "tbl") + + _ = m.PutItem(ctx, "tbl", map[string]any{"pk": "u1", "sk": "info", "v": 1}) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "tbl", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{{Action: "ADD", Field: "v", Value: 1}}, + }) + assertError(t, err, true) +} + +func TestUpdateItemEmitsStreamRecord(t *testing.T) { + m := newTestMock() + ctx := context.Background() + createTestTable(m, "tbl") + + _ = m.UpdateStreamConfig(ctx, "tbl", driver.StreamConfig{ + Enabled: true, ViewType: "NEW_AND_OLD_IMAGES", + }) + + _ = m.PutItem(ctx, "tbl", map[string]any{"pk": "u1", "sk": "info", "val": "old"}) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "tbl", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "val", Value: "new"}, + }, + }) + requireNoError(t, err) + + iter, err := m.GetStreamRecords(ctx, "tbl", 10, "") + requireNoError(t, err) + assertEqual(t, 2, len(iter.Records)) + assertEqual(t, "MODIFY", iter.Records[1].EventType) + assertEqual(t, "old", iter.Records[1].OldImage["val"]) + assertEqual(t, "new", iter.Records[1].NewImage["val"]) +} + +func TestUpdateItemEmitsMetrics(t *testing.T) { + fc := config.NewFakeClock(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) + opts := config.NewOptions(config.WithClock(fc)) + m := New(opts) + ctx := context.Background() + + cw := cloudwatch.New(opts) + m.SetMonitoring(cw) + createTestTable(m, "tbl") + + _ = m.PutItem(ctx, "tbl", map[string]any{"pk": "u1", "sk": "info"}) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "tbl", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "v", Value: "x"}, + }, + }) + requireNoError(t, err) + + result, err := cw.GetMetricData(ctx, mondriver.GetMetricInput{ + Namespace: "AWS/DynamoDB", + MetricName: "ConsumedWriteCapacityUnits", + Dimensions: map[string]string{"TableName": "tbl"}, + StartTime: fc.Now().Add(-1 * time.Hour), + EndTime: fc.Now().Add(1 * time.Hour), + Period: 60, + Stat: "Sum", + }) + requireNoError(t, err) + assertEqual(t, true, len(result.Values) > 0) +} diff --git a/providers/azure/cosmosdb/cosmosdb_test.go b/providers/azure/cosmosdb/cosmosdb_test.go index f779d26c..aa6db9e0 100644 --- a/providers/azure/cosmosdb/cosmosdb_test.go +++ b/providers/azure/cosmosdb/cosmosdb_test.go @@ -687,3 +687,187 @@ func (c *cosmosMetricsCollector) hasMetric(namespace, metricName string) bool { } return false } + +func TestUpdateItemSetFields(t *testing.T) { + ctx := context.Background() + m := newTestMock() + createTestTable(t, m) + + require.NoError(t, m.PutItem(ctx, "users", map[string]any{ + "pk": "u1", "sk": "info", "name": "Alice", "age": 30, + })) + + updated, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "name", Value: "Alice Smith"}, + {Action: "SET", Field: "email", Value: "alice@test.com"}, + }, + }) + require.NoError(t, err) + assert.Equal(t, "Alice Smith", updated["name"]) + assert.Equal(t, "alice@test.com", updated["email"]) + assert.Equal(t, 30, updated["age"]) +} + +func TestUpdateItemRemoveFields(t *testing.T) { + ctx := context.Background() + m := newTestMock() + createTestTable(t, m) + + require.NoError(t, m.PutItem(ctx, "users", map[string]any{ + "pk": "u1", "sk": "info", "name": "Alice", "city": "NYC", + })) + + updated, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "REMOVE", Field: "city"}, + }, + }) + require.NoError(t, err) + _, hasCityField := updated["city"] + assert.False(t, hasCityField, "expected city to be removed") + assert.Equal(t, "Alice", updated["name"]) +} + +func TestUpdateItemSetAndRemoveCombined(t *testing.T) { + ctx := context.Background() + m := newTestMock() + createTestTable(t, m) + + require.NoError(t, m.PutItem(ctx, "users", map[string]any{ + "pk": "u1", "sk": "info", "name": "Alice", "old_field": "remove_me", + })) + + updated, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "name", Value: "Bob"}, + {Action: "REMOVE", Field: "old_field"}, + }, + }) + require.NoError(t, err) + assert.Equal(t, "Bob", updated["name"]) + _, hasOld := updated["old_field"] + assert.False(t, hasOld, "expected old_field to be removed") +} + +func TestUpdateItemPersistsChanges(t *testing.T) { + ctx := context.Background() + m := newTestMock() + createTestTable(t, m) + + require.NoError(t, m.PutItem(ctx, "users", map[string]any{ + "pk": "u1", "sk": "info", "v": "old", + })) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "v", Value: "new"}, + }, + }) + require.NoError(t, err) + + got, err := m.GetItem(ctx, "users", map[string]any{"pk": "u1", "sk": "info"}) + require.NoError(t, err) + assert.Equal(t, "new", got["v"]) +} + +func TestUpdateItemTableNotFound(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "nonexistent", + Key: map[string]any{"pk": "x"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "v", Value: 1}}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + +func TestUpdateItemItemNotFound(t *testing.T) { + ctx := context.Background() + m := newTestMock() + createTestTable(t, m) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "missing", "sk": "missing"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "v", Value: 1}}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + +func TestUpdateItemInvalidAction(t *testing.T) { + ctx := context.Background() + m := newTestMock() + createTestTable(t, m) + + require.NoError(t, m.PutItem(ctx, "users", map[string]any{"pk": "u1", "sk": "info", "v": 1})) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{{Action: "ADD", Field: "v", Value: 1}}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported") +} + +func TestUpdateItemEmitsStreamRecord(t *testing.T) { + ctx := context.Background() + m := newTestMock() + createTestTable(t, m) + + require.NoError(t, m.UpdateStreamConfig(ctx, "users", driver.StreamConfig{ + Enabled: true, ViewType: "NEW_AND_OLD_IMAGES", + })) + + require.NoError(t, m.PutItem(ctx, "users", map[string]any{"pk": "u1", "sk": "info", "val": "old"})) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "val", Value: "new"}, + }, + }) + require.NoError(t, err) + + iter, err := m.GetStreamRecords(ctx, "users", 10, "") + require.NoError(t, err) + require.Len(t, iter.Records, 2) + assert.Equal(t, "MODIFY", iter.Records[1].EventType) + assert.Equal(t, "old", iter.Records[1].OldImage["val"]) + assert.Equal(t, "new", iter.Records[1].NewImage["val"]) +} + +func TestUpdateItemEmitsMetrics(t *testing.T) { + ctx := context.Background() + m := newTestMock() + mon := &cosmosMetricsCollector{} + m.SetMonitoring(mon) + createTestTable(t, m) + + require.NoError(t, m.PutItem(ctx, "users", map[string]any{"pk": "u1", "sk": "info"})) + + mon.reset() + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "users", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "v", Value: "x"}, + }, + }) + require.NoError(t, err) + assert.True(t, mon.hasMetric("Microsoft.DocumentDB/databaseAccounts", "TotalRequests")) +} diff --git a/providers/gcp/firestore/firestore_test.go b/providers/gcp/firestore/firestore_test.go index f29e7a82..4f3245a9 100644 --- a/providers/gcp/firestore/firestore_test.go +++ b/providers/gcp/firestore/firestore_test.go @@ -954,3 +954,192 @@ func TestScanUnsupportedFilter(t *testing.T) { require.NoError(t, err) assert.Equal(t, 0, result.Count) } + +func TestUpdateItemSetFields(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + require.NoError(t, m.CreateTable(ctx, driver.TableConfig{Name: "col", PartitionKey: "pk", SortKey: "sk"})) + require.NoError(t, m.PutItem(ctx, "col", map[string]any{ + "pk": "u1", "sk": "info", "name": "Alice", "age": 30, + })) + + updated, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "col", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "name", Value: "Alice Smith"}, + {Action: "SET", Field: "email", Value: "alice@test.com"}, + }, + }) + require.NoError(t, err) + assert.Equal(t, "Alice Smith", updated["name"]) + assert.Equal(t, "alice@test.com", updated["email"]) + assert.Equal(t, 30, updated["age"]) +} + +func TestUpdateItemRemoveFields(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + require.NoError(t, m.CreateTable(ctx, driver.TableConfig{Name: "col", PartitionKey: "pk", SortKey: "sk"})) + require.NoError(t, m.PutItem(ctx, "col", map[string]any{ + "pk": "u1", "sk": "info", "name": "Alice", "city": "NYC", + })) + + updated, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "col", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "REMOVE", Field: "city"}, + }, + }) + require.NoError(t, err) + _, hasCityField := updated["city"] + assert.False(t, hasCityField, "expected city to be removed") + assert.Equal(t, "Alice", updated["name"]) +} + +func TestUpdateItemSetAndRemoveCombined(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + require.NoError(t, m.CreateTable(ctx, driver.TableConfig{Name: "col", PartitionKey: "pk", SortKey: "sk"})) + require.NoError(t, m.PutItem(ctx, "col", map[string]any{ + "pk": "u1", "sk": "info", "name": "Alice", "old_field": "remove_me", + })) + + updated, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "col", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "name", Value: "Bob"}, + {Action: "REMOVE", Field: "old_field"}, + }, + }) + require.NoError(t, err) + assert.Equal(t, "Bob", updated["name"]) + _, hasOld := updated["old_field"] + assert.False(t, hasOld, "expected old_field to be removed") +} + +func TestUpdateItemPersistsChanges(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + require.NoError(t, m.CreateTable(ctx, driver.TableConfig{Name: "col", PartitionKey: "pk", SortKey: "sk"})) + require.NoError(t, m.PutItem(ctx, "col", map[string]any{ + "pk": "u1", "sk": "info", "v": "old", + })) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "col", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "v", Value: "new"}, + }, + }) + require.NoError(t, err) + + got, err := m.GetItem(ctx, "col", map[string]any{"pk": "u1", "sk": "info"}) + require.NoError(t, err) + assert.Equal(t, "new", got["v"]) +} + +func TestUpdateItemCollectionNotFound(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "nonexistent", + Key: map[string]any{"pk": "x"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "v", Value: 1}}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + +func TestUpdateItemDocumentNotFound(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + require.NoError(t, m.CreateTable(ctx, driver.TableConfig{Name: "col", PartitionKey: "pk", SortKey: "sk"})) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "col", + Key: map[string]any{"pk": "missing", "sk": "missing"}, + Actions: []driver.UpdateAction{{Action: "SET", Field: "v", Value: 1}}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + +func TestUpdateItemInvalidAction(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + require.NoError(t, m.CreateTable(ctx, driver.TableConfig{Name: "col", PartitionKey: "pk", SortKey: "sk"})) + require.NoError(t, m.PutItem(ctx, "col", map[string]any{"pk": "u1", "sk": "info", "v": 1})) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "col", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{{Action: "ADD", Field: "v", Value: 1}}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported") +} + +func TestUpdateItemEmitsStreamRecord(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + require.NoError(t, m.CreateTable(ctx, driver.TableConfig{Name: "col", PartitionKey: "pk", SortKey: "sk"})) + require.NoError(t, m.UpdateStreamConfig(ctx, "col", driver.StreamConfig{ + Enabled: true, ViewType: "NEW_AND_OLD_IMAGES", + })) + + require.NoError(t, m.PutItem(ctx, "col", map[string]any{"pk": "u1", "sk": "info", "val": "old"})) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "col", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "val", Value: "new"}, + }, + }) + require.NoError(t, err) + + iter, err := m.GetStreamRecords(ctx, "col", 10, "") + require.NoError(t, err) + require.Len(t, iter.Records, 2) + assert.Equal(t, "MODIFY", iter.Records[1].EventType) + assert.Equal(t, "old", iter.Records[1].OldImage["val"]) + assert.Equal(t, "new", iter.Records[1].NewImage["val"]) +} + +func TestUpdateItemEmitsMetrics(t *testing.T) { + ctx := context.Background() + clk := config.NewFakeClock(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) + opts := config.NewOptions(config.WithClock(clk), config.WithProjectID("test-project")) + + mon := &firestoreMonMock{data: make(map[string][]mondriver.MetricDatum)} + m := New(opts) + m.SetMonitoring(mon) + + require.NoError(t, m.CreateTable(ctx, driver.TableConfig{Name: "col", PartitionKey: "pk", SortKey: "sk"})) + require.NoError(t, m.PutItem(ctx, "col", map[string]any{"pk": "u1", "sk": "info"})) + + // Clear metrics from PutItem + mon.data = make(map[string][]mondriver.MetricDatum) + + _, err := m.UpdateItem(ctx, driver.UpdateItemInput{ + Table: "col", + Key: map[string]any{"pk": "u1", "sk": "info"}, + Actions: []driver.UpdateAction{ + {Action: "SET", Field: "v", Value: "x"}, + }, + }) + require.NoError(t, err) + assert.NotEmpty(t, mon.data["firestore.googleapis.com/document/write_count"]) +} From c8688cb3d70c43a42cf81cd9927598faeab28f03 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Tue, 31 Mar 2026 15:21:37 +0530 Subject: [PATCH 5/8] add TTL management and atomic counters to cache service Closes #64 --- cache/cache.go | 78 +++++ cache/driver/driver.go | 11 + cloudemu_test.go | 276 ++++++++++++++++++ providers/aws/elasticache/elasticache.go | 131 +++++++++ providers/aws/elasticache/elasticache_test.go | 157 ++++++++++ providers/azure/azurecache/azurecache.go | 131 +++++++++ providers/azure/azurecache/azurecache_test.go | 120 ++++++++ providers/gcp/memorystore/memorystore.go | 131 +++++++++ providers/gcp/memorystore/memorystore_test.go | 120 ++++++++ 9 files changed, 1155 insertions(+) diff --git a/cache/cache.go b/cache/cache.go index 830769ff..10814480 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -178,3 +178,81 @@ func (c *Cache) FlushAll(ctx context.Context, cacheName string) error { _, err := c.do(ctx, "FlushAll", cacheName, func() (any, error) { return nil, c.driver.FlushAll(ctx, cacheName) }) return err } + +// Expire sets a TTL on an existing key. +func (c *Cache) Expire(ctx context.Context, cacheName, key string, ttl time.Duration) error { + _, err := c.do(ctx, "Expire", map[string]string{"cache": cacheName, "key": key}, func() (any, error) { + return nil, c.driver.Expire(ctx, cacheName, key, ttl) + }) + + return err +} + +// GetTTL returns the remaining TTL for a key. Returns -1 if the key has no TTL. +func (c *Cache) GetTTL(ctx context.Context, cacheName, key string) (time.Duration, error) { + out, err := c.do(ctx, "GetTTL", map[string]string{"cache": cacheName, "key": key}, func() (any, error) { + return c.driver.GetTTL(ctx, cacheName, key) + }) + if err != nil { + return 0, err + } + + return out.(time.Duration), nil +} + +// Persist removes the TTL from a key, making it persistent. +func (c *Cache) Persist(ctx context.Context, cacheName, key string) error { + _, err := c.do(ctx, "Persist", map[string]string{"cache": cacheName, "key": key}, func() (any, error) { + return nil, c.driver.Persist(ctx, cacheName, key) + }) + + return err +} + +// Incr atomically increments the integer value of a key by 1. +func (c *Cache) Incr(ctx context.Context, cacheName, key string) (int64, error) { + out, err := c.do(ctx, "Incr", map[string]string{"cache": cacheName, "key": key}, func() (any, error) { + return c.driver.Incr(ctx, cacheName, key) + }) + if err != nil { + return 0, err + } + + return out.(int64), nil +} + +// IncrBy atomically increments the integer value of a key by delta. +func (c *Cache) IncrBy(ctx context.Context, cacheName, key string, delta int64) (int64, error) { + out, err := c.do(ctx, "IncrBy", map[string]string{"cache": cacheName, "key": key}, func() (any, error) { + return c.driver.IncrBy(ctx, cacheName, key, delta) + }) + if err != nil { + return 0, err + } + + return out.(int64), nil +} + +// Decr atomically decrements the integer value of a key by 1. +func (c *Cache) Decr(ctx context.Context, cacheName, key string) (int64, error) { + out, err := c.do(ctx, "Decr", map[string]string{"cache": cacheName, "key": key}, func() (any, error) { + return c.driver.Decr(ctx, cacheName, key) + }) + if err != nil { + return 0, err + } + + return out.(int64), nil +} + +// DecrBy atomically decrements the integer value of a key by delta. +func (c *Cache) DecrBy(ctx context.Context, cacheName, key string, delta int64) (int64, error) { + out, err := c.do(ctx, "DecrBy", map[string]string{"cache": cacheName, "key": key}, func() (any, error) { + return c.driver.DecrBy(ctx, cacheName, key, delta) + }) + if err != nil { + return 0, err + } + + return out.(int64), nil +} diff --git a/cache/driver/driver.go b/cache/driver/driver.go index 9c9bfc8d..0671087f 100644 --- a/cache/driver/driver.go +++ b/cache/driver/driver.go @@ -45,4 +45,15 @@ type Cache interface { Delete(ctx context.Context, cacheName, key string) error Keys(ctx context.Context, cacheName, pattern string) ([]string, error) FlushAll(ctx context.Context, cacheName string) error + + // TTL management + Expire(ctx context.Context, cacheName, key string, ttl time.Duration) error + GetTTL(ctx context.Context, cacheName, key string) (time.Duration, error) + Persist(ctx context.Context, cacheName, key string) error + + // Atomic counters + Incr(ctx context.Context, cacheName, key string) (int64, error) + IncrBy(ctx context.Context, cacheName, key string, delta int64) (int64, error) + Decr(ctx context.Context, cacheName, key string) (int64, error) + DecrBy(ctx context.Context, cacheName, key string, delta int64) (int64, error) } diff --git a/cloudemu_test.go b/cloudemu_test.go index 69233a3a..37f074b5 100644 --- a/cloudemu_test.go +++ b/cloudemu_test.go @@ -5968,3 +5968,279 @@ func TestUpdateItemWithStreams(t *testing.T) { t.Errorf("expected new image val='new', got %v", modifyRec.NewImage["val"]) } } + +func TestCacheExpireAndPersistAWS(t *testing.T) { + ctx := context.Background() + p := NewAWS() + + _, err := p.ElastiCache.CreateCache(ctx, cachedriver.CacheConfig{Name: "c1"}) + if err != nil { + t.Fatal(err) + } + + if err := p.ElastiCache.Set(ctx, "c1", "k1", []byte("val"), 0); err != nil { + t.Fatal(err) + } + + ttl, err := p.ElastiCache.GetTTL(ctx, "c1", "k1") + if err != nil { + t.Fatal(err) + } + + if ttl != -1 { + t.Errorf("expected TTL -1, got %v", ttl) + } + + if err := p.ElastiCache.Expire(ctx, "c1", "k1", 1*time.Hour); err != nil { + t.Fatal(err) + } + + ttl, err = p.ElastiCache.GetTTL(ctx, "c1", "k1") + if err != nil { + t.Fatal(err) + } + + if ttl <= 0 { + t.Errorf("expected positive TTL, got %v", ttl) + } + + if err := p.ElastiCache.Persist(ctx, "c1", "k1"); err != nil { + t.Fatal(err) + } + + ttl, err = p.ElastiCache.GetTTL(ctx, "c1", "k1") + if err != nil { + t.Fatal(err) + } + + if ttl != -1 { + t.Errorf("expected TTL -1 after Persist, got %v", ttl) + } +} + +func TestCacheExpireAndPersistAzure(t *testing.T) { + ctx := context.Background() + p := NewAzure() + + _, err := p.Cache.CreateCache(ctx, cachedriver.CacheConfig{Name: "c1"}) + if err != nil { + t.Fatal(err) + } + + if err := p.Cache.Set(ctx, "c1", "k1", []byte("val"), 0); err != nil { + t.Fatal(err) + } + + if err := p.Cache.Expire(ctx, "c1", "k1", 1*time.Hour); err != nil { + t.Fatal(err) + } + + ttl, err := p.Cache.GetTTL(ctx, "c1", "k1") + if err != nil { + t.Fatal(err) + } + + if ttl <= 0 { + t.Errorf("expected positive TTL, got %v", ttl) + } + + if err := p.Cache.Persist(ctx, "c1", "k1"); err != nil { + t.Fatal(err) + } + + ttl, err = p.Cache.GetTTL(ctx, "c1", "k1") + if err != nil { + t.Fatal(err) + } + + if ttl != -1 { + t.Errorf("expected TTL -1 after Persist, got %v", ttl) + } +} + +func TestCacheExpireAndPersistGCP(t *testing.T) { + ctx := context.Background() + p := NewGCP() + + _, err := p.Memorystore.CreateCache(ctx, cachedriver.CacheConfig{Name: "c1"}) + if err != nil { + t.Fatal(err) + } + + if err := p.Memorystore.Set(ctx, "c1", "k1", []byte("val"), 0); err != nil { + t.Fatal(err) + } + + if err := p.Memorystore.Expire(ctx, "c1", "k1", 1*time.Hour); err != nil { + t.Fatal(err) + } + + ttl, err := p.Memorystore.GetTTL(ctx, "c1", "k1") + if err != nil { + t.Fatal(err) + } + + if ttl <= 0 { + t.Errorf("expected positive TTL, got %v", ttl) + } + + if err := p.Memorystore.Persist(ctx, "c1", "k1"); err != nil { + t.Fatal(err) + } + + ttl, err = p.Memorystore.GetTTL(ctx, "c1", "k1") + if err != nil { + t.Fatal(err) + } + + if ttl != -1 { + t.Errorf("expected TTL -1 after Persist, got %v", ttl) + } +} + +func TestCacheIncrDecrAWS(t *testing.T) { + ctx := context.Background() + p := NewAWS() + + _, err := p.ElastiCache.CreateCache(ctx, cachedriver.CacheConfig{Name: "c1"}) + if err != nil { + t.Fatal(err) + } + + val, err := p.ElastiCache.Incr(ctx, "c1", "counter") + if err != nil { + t.Fatal(err) + } + + if val != 1 { + t.Errorf("expected 1, got %d", val) + } + + val, err = p.ElastiCache.IncrBy(ctx, "c1", "counter", 9) + if err != nil { + t.Fatal(err) + } + + if val != 10 { + t.Errorf("expected 10, got %d", val) + } + + val, err = p.ElastiCache.Decr(ctx, "c1", "counter") + if err != nil { + t.Fatal(err) + } + + if val != 9 { + t.Errorf("expected 9, got %d", val) + } + + val, err = p.ElastiCache.DecrBy(ctx, "c1", "counter", 4) + if err != nil { + t.Fatal(err) + } + + if val != 5 { + t.Errorf("expected 5, got %d", val) + } + + item, err := p.ElastiCache.Get(ctx, "c1", "counter") + if err != nil { + t.Fatal(err) + } + + if string(item.Value) != "5" { + t.Errorf("expected '5', got %q", string(item.Value)) + } +} + +func TestCacheIncrDecrAzure(t *testing.T) { + ctx := context.Background() + p := NewAzure() + + _, err := p.Cache.CreateCache(ctx, cachedriver.CacheConfig{Name: "c1"}) + if err != nil { + t.Fatal(err) + } + + val, err := p.Cache.Incr(ctx, "c1", "counter") + if err != nil { + t.Fatal(err) + } + + if val != 1 { + t.Errorf("expected 1, got %d", val) + } + + val, err = p.Cache.IncrBy(ctx, "c1", "counter", 9) + if err != nil { + t.Fatal(err) + } + + if val != 10 { + t.Errorf("expected 10, got %d", val) + } + + val, err = p.Cache.Decr(ctx, "c1", "counter") + if err != nil { + t.Fatal(err) + } + + if val != 9 { + t.Errorf("expected 9, got %d", val) + } + + val, err = p.Cache.DecrBy(ctx, "c1", "counter", 4) + if err != nil { + t.Fatal(err) + } + + if val != 5 { + t.Errorf("expected 5, got %d", val) + } +} + +func TestCacheIncrDecrGCP(t *testing.T) { + ctx := context.Background() + p := NewGCP() + + _, err := p.Memorystore.CreateCache(ctx, cachedriver.CacheConfig{Name: "c1"}) + if err != nil { + t.Fatal(err) + } + + val, err := p.Memorystore.Incr(ctx, "c1", "counter") + if err != nil { + t.Fatal(err) + } + + if val != 1 { + t.Errorf("expected 1, got %d", val) + } + + val, err = p.Memorystore.IncrBy(ctx, "c1", "counter", 9) + if err != nil { + t.Fatal(err) + } + + if val != 10 { + t.Errorf("expected 10, got %d", val) + } + + val, err = p.Memorystore.Decr(ctx, "c1", "counter") + if err != nil { + t.Fatal(err) + } + + if val != 9 { + t.Errorf("expected 9, got %d", val) + } + + val, err = p.Memorystore.DecrBy(ctx, "c1", "counter", 4) + if err != nil { + t.Fatal(err) + } + + if val != 5 { + t.Errorf("expected 5, got %d", val) + } +} diff --git a/providers/aws/elasticache/elasticache.go b/providers/aws/elasticache/elasticache.go index 28bab1e0..b868f705 100644 --- a/providers/aws/elasticache/elasticache.go +++ b/providers/aws/elasticache/elasticache.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "path" + "strconv" "time" "github.com/stackshy/cloudemu/cache/driver" @@ -282,6 +283,136 @@ func (m *Mock) FlushAll(_ context.Context, cacheName string) error { return nil } +// Expire sets a TTL on an existing key. +func (m *Mock) Expire(_ context.Context, cacheName, key string, ttl time.Duration) error { + cd, ok := m.caches.Get(cacheName) + if !ok { + return errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + item, ok := cd.items.Get(key) + if !ok || (item.HasTTL && m.opts.Clock.Now().After(item.ExpiresAt)) { + return errors.Newf(errors.NotFound, "key %q not found in cache %q", key, cacheName) + } + + item.HasTTL = true + item.ExpiresAt = m.opts.Clock.Now().Add(ttl) + cd.items.Set(key, item) + + return nil +} + +// GetTTL returns the remaining TTL for a key. Returns -1 if the key has no TTL. +func (m *Mock) GetTTL(_ context.Context, cacheName, key string) (time.Duration, error) { + cd, ok := m.caches.Get(cacheName) + if !ok { + return 0, errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + item, ok := cd.items.Get(key) + if !ok || (item.HasTTL && m.opts.Clock.Now().After(item.ExpiresAt)) { + return 0, errors.Newf(errors.NotFound, "key %q not found in cache %q", key, cacheName) + } + + if !item.HasTTL { + return -1, nil + } + + return item.ExpiresAt.Sub(m.opts.Clock.Now()), nil +} + +// Persist removes the TTL from a key, making it persistent. +func (m *Mock) Persist(_ context.Context, cacheName, key string) error { + cd, ok := m.caches.Get(cacheName) + if !ok { + return errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + item, ok := cd.items.Get(key) + if !ok || (item.HasTTL && m.opts.Clock.Now().After(item.ExpiresAt)) { + return errors.Newf(errors.NotFound, "key %q not found in cache %q", key, cacheName) + } + + item.HasTTL = false + item.ExpiresAt = time.Time{} + cd.items.Set(key, item) + + return nil +} + +// Incr atomically increments the integer value of a key by 1. +func (m *Mock) Incr(ctx context.Context, cacheName, key string) (int64, error) { + return m.IncrBy(ctx, cacheName, key, 1) +} + +// IncrBy atomically increments the integer value of a key by delta. +func (m *Mock) IncrBy(_ context.Context, cacheName, key string, delta int64) (int64, error) { + cd, ok := m.caches.Get(cacheName) + if !ok { + return 0, errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + newVal, err := applyDelta(cd, key, delta, m.opts.Clock.Now()) + if err != nil { + return 0, err + } + + m.emitMetric("IncrCommands", 1, map[string]string{"CacheClusterId": cacheName}) + + return newVal, nil +} + +// Decr atomically decrements the integer value of a key by 1. +func (m *Mock) Decr(ctx context.Context, cacheName, key string) (int64, error) { + return m.DecrBy(ctx, cacheName, key, 1) +} + +// DecrBy atomically decrements the integer value of a key by delta. +func (m *Mock) DecrBy(_ context.Context, cacheName, key string, delta int64) (int64, error) { + cd, ok := m.caches.Get(cacheName) + if !ok { + return 0, errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + newVal, err := applyDelta(cd, key, -delta, m.opts.Clock.Now()) + if err != nil { + return 0, err + } + + m.emitMetric("DecrCommands", 1, map[string]string{"CacheClusterId": cacheName}) + + return newVal, nil +} + +func applyDelta(cd *cacheData, key string, delta int64, now time.Time) (int64, error) { + item, ok := cd.items.Get(key) + + var current int64 + + if ok && (!item.HasTTL || !now.After(item.ExpiresAt)) { + val, err := strconv.ParseInt(string(item.Value), 10, 64) + if err != nil { + return 0, errors.New(errors.InvalidArgument, "value is not an integer") + } + + current = val + } + + newVal := current + delta + newItem := cacheItem{ + Value: []byte(strconv.FormatInt(newVal, 10)), + } + + if ok && item.HasTTL && !now.After(item.ExpiresAt) { + newItem.HasTTL = true + newItem.ExpiresAt = item.ExpiresAt + } + + cd.items.Set(key, newItem) + + return newVal, nil +} + // matchPattern matches a key against a glob-like pattern. // Supports full glob syntax including middle wildcards like "user:*:session". func matchPattern(pattern, key string) bool { diff --git a/providers/aws/elasticache/elasticache_test.go b/providers/aws/elasticache/elasticache_test.go index 153b83dc..a58ceac1 100644 --- a/providers/aws/elasticache/elasticache_test.go +++ b/providers/aws/elasticache/elasticache_test.go @@ -375,3 +375,160 @@ func TestMatchPattern(t *testing.T) { }) } } + +func TestExpire(t *testing.T) { + m, fc := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "k1", []byte("val"), 0)) + + ttl, err := m.GetTTL(ctx, "c1", "k1") + require.NoError(t, err) + assert.Equal(t, time.Duration(-1), ttl) + + require.NoError(t, m.Expire(ctx, "c1", "k1", 1*time.Hour)) + + ttl, err = m.GetTTL(ctx, "c1", "k1") + require.NoError(t, err) + assert.True(t, ttl > 0 && ttl <= 1*time.Hour) + + fc.Advance(2 * time.Hour) + + _, err = m.Get(ctx, "c1", "k1") + require.Error(t, err) +} + +func TestExpireKeyNotFound(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + err := m.Expire(ctx, "c1", "missing", 1*time.Hour) + require.Error(t, err) +} + +func TestGetTTLKeyNotFound(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + _, err := m.GetTTL(ctx, "c1", "missing") + require.Error(t, err) +} + +func TestPersist(t *testing.T) { + m, fc := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "k1", []byte("val"), 1*time.Hour)) + + require.NoError(t, m.Persist(ctx, "c1", "k1")) + + ttl, err := m.GetTTL(ctx, "c1", "k1") + require.NoError(t, err) + assert.Equal(t, time.Duration(-1), ttl) + + fc.Advance(2 * time.Hour) + + item, err := m.Get(ctx, "c1", "k1") + require.NoError(t, err) + assert.Equal(t, []byte("val"), item.Value) +} + +func TestPersistKeyNotFound(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + err := m.Persist(ctx, "c1", "missing") + require.Error(t, err) +} + +func TestIncr(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + val, err := m.Incr(ctx, "c1", "counter") + require.NoError(t, err) + assert.Equal(t, int64(1), val) + + val, err = m.Incr(ctx, "c1", "counter") + require.NoError(t, err) + assert.Equal(t, int64(2), val) +} + +func TestIncrBy(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "counter", []byte("10"), 0)) + + val, err := m.IncrBy(ctx, "c1", "counter", 5) + require.NoError(t, err) + assert.Equal(t, int64(15), val) +} + +func TestDecr(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "counter", []byte("10"), 0)) + + val, err := m.Decr(ctx, "c1", "counter") + require.NoError(t, err) + assert.Equal(t, int64(9), val) +} + +func TestDecrBy(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "counter", []byte("20"), 0)) + + val, err := m.DecrBy(ctx, "c1", "counter", 7) + require.NoError(t, err) + assert.Equal(t, int64(13), val) +} + +func TestIncrNonInteger(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "k1", []byte("not-a-number"), 0)) + + _, err := m.Incr(ctx, "c1", "k1") + require.Error(t, err) + assert.Contains(t, err.Error(), "not an integer") +} + +func TestIncrPreservesTTL(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "counter", []byte("5"), 1*time.Hour)) + + val, err := m.IncrBy(ctx, "c1", "counter", 3) + require.NoError(t, err) + assert.Equal(t, int64(8), val) + + ttl, err := m.GetTTL(ctx, "c1", "counter") + require.NoError(t, err) + assert.True(t, ttl > 0) +} + +func TestIncrCacheNotFound(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + + _, err := m.Incr(ctx, "nonexistent", "k1") + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} diff --git a/providers/azure/azurecache/azurecache.go b/providers/azure/azurecache/azurecache.go index e1bbe47a..62d610d5 100644 --- a/providers/azure/azurecache/azurecache.go +++ b/providers/azure/azurecache/azurecache.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "path" + "strconv" "time" "github.com/stackshy/cloudemu/cache/driver" @@ -289,6 +290,136 @@ func (m *Mock) FlushAll(_ context.Context, cacheName string) error { return nil } +// Expire sets a TTL on an existing key. +func (m *Mock) Expire(_ context.Context, cacheName, key string, ttl time.Duration) error { + cd, ok := m.caches.Get(cacheName) + if !ok { + return errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + item, ok := cd.items.Get(key) + if !ok || (item.HasTTL && m.opts.Clock.Now().After(item.ExpiresAt)) { + return errors.Newf(errors.NotFound, "key %q not found in cache %q", key, cacheName) + } + + item.HasTTL = true + item.ExpiresAt = m.opts.Clock.Now().Add(ttl) + cd.items.Set(key, item) + + return nil +} + +// GetTTL returns the remaining TTL for a key. Returns -1 if the key has no TTL. +func (m *Mock) GetTTL(_ context.Context, cacheName, key string) (time.Duration, error) { + cd, ok := m.caches.Get(cacheName) + if !ok { + return 0, errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + item, ok := cd.items.Get(key) + if !ok || (item.HasTTL && m.opts.Clock.Now().After(item.ExpiresAt)) { + return 0, errors.Newf(errors.NotFound, "key %q not found in cache %q", key, cacheName) + } + + if !item.HasTTL { + return -1, nil + } + + return item.ExpiresAt.Sub(m.opts.Clock.Now()), nil +} + +// Persist removes the TTL from a key, making it persistent. +func (m *Mock) Persist(_ context.Context, cacheName, key string) error { + cd, ok := m.caches.Get(cacheName) + if !ok { + return errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + item, ok := cd.items.Get(key) + if !ok || (item.HasTTL && m.opts.Clock.Now().After(item.ExpiresAt)) { + return errors.Newf(errors.NotFound, "key %q not found in cache %q", key, cacheName) + } + + item.HasTTL = false + item.ExpiresAt = time.Time{} + cd.items.Set(key, item) + + return nil +} + +// Incr atomically increments the integer value of a key by 1. +func (m *Mock) Incr(ctx context.Context, cacheName, key string) (int64, error) { + return m.IncrBy(ctx, cacheName, key, 1) +} + +// IncrBy atomically increments the integer value of a key by delta. +func (m *Mock) IncrBy(_ context.Context, cacheName, key string, delta int64) (int64, error) { + cd, ok := m.caches.Get(cacheName) + if !ok { + return 0, errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + newVal, err := applyDelta(cd, key, delta, m.opts.Clock.Now()) + if err != nil { + return 0, err + } + + m.emitMetric(cacheName, map[string]float64{"TotalCommandsProcessed": 1}) + + return newVal, nil +} + +// Decr atomically decrements the integer value of a key by 1. +func (m *Mock) Decr(ctx context.Context, cacheName, key string) (int64, error) { + return m.DecrBy(ctx, cacheName, key, 1) +} + +// DecrBy atomically decrements the integer value of a key by delta. +func (m *Mock) DecrBy(_ context.Context, cacheName, key string, delta int64) (int64, error) { + cd, ok := m.caches.Get(cacheName) + if !ok { + return 0, errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + newVal, err := applyDelta(cd, key, -delta, m.opts.Clock.Now()) + if err != nil { + return 0, err + } + + m.emitMetric(cacheName, map[string]float64{"TotalCommandsProcessed": 1}) + + return newVal, nil +} + +func applyDelta(cd *cacheData, key string, delta int64, now time.Time) (int64, error) { + item, ok := cd.items.Get(key) + + var current int64 + + if ok && (!item.HasTTL || !now.After(item.ExpiresAt)) { + val, err := strconv.ParseInt(string(item.Value), 10, 64) + if err != nil { + return 0, errors.New(errors.InvalidArgument, "value is not an integer") + } + + current = val + } + + newVal := current + delta + newItem := cacheItem{ + Value: []byte(strconv.FormatInt(newVal, 10)), + } + + if ok && item.HasTTL && !now.After(item.ExpiresAt) { + newItem.HasTTL = true + newItem.ExpiresAt = item.ExpiresAt + } + + cd.items.Set(key, newItem) + + return newVal, nil +} + // matchPattern matches a key against a glob-like pattern. // Supports full glob syntax including middle wildcards like "user:*:session". func matchPattern(pattern, key string) bool { diff --git a/providers/azure/azurecache/azurecache_test.go b/providers/azure/azurecache/azurecache_test.go index 06dfae89..3da43c5a 100644 --- a/providers/azure/azurecache/azurecache_test.go +++ b/providers/azure/azurecache/azurecache_test.go @@ -375,3 +375,123 @@ func TestMatchPattern(t *testing.T) { }) } } + +func TestExpire(t *testing.T) { + m, fc := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "k1", []byte("val"), 0)) + + ttl, err := m.GetTTL(ctx, "c1", "k1") + require.NoError(t, err) + assert.Equal(t, time.Duration(-1), ttl) + + require.NoError(t, m.Expire(ctx, "c1", "k1", 1*time.Hour)) + + ttl, err = m.GetTTL(ctx, "c1", "k1") + require.NoError(t, err) + assert.True(t, ttl > 0 && ttl <= 1*time.Hour) + + fc.Advance(2 * time.Hour) + + _, err = m.Get(ctx, "c1", "k1") + require.Error(t, err) +} + +func TestPersist(t *testing.T) { + m, fc := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "k1", []byte("val"), 1*time.Hour)) + require.NoError(t, m.Persist(ctx, "c1", "k1")) + + ttl, err := m.GetTTL(ctx, "c1", "k1") + require.NoError(t, err) + assert.Equal(t, time.Duration(-1), ttl) + + fc.Advance(2 * time.Hour) + + item, err := m.Get(ctx, "c1", "k1") + require.NoError(t, err) + assert.Equal(t, []byte("val"), item.Value) +} + +func TestIncr(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + val, err := m.Incr(ctx, "c1", "counter") + require.NoError(t, err) + assert.Equal(t, int64(1), val) + + val, err = m.Incr(ctx, "c1", "counter") + require.NoError(t, err) + assert.Equal(t, int64(2), val) +} + +func TestIncrBy(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "counter", []byte("10"), 0)) + + val, err := m.IncrBy(ctx, "c1", "counter", 5) + require.NoError(t, err) + assert.Equal(t, int64(15), val) +} + +func TestDecr(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "counter", []byte("10"), 0)) + + val, err := m.Decr(ctx, "c1", "counter") + require.NoError(t, err) + assert.Equal(t, int64(9), val) +} + +func TestDecrBy(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "counter", []byte("20"), 0)) + + val, err := m.DecrBy(ctx, "c1", "counter", 7) + require.NoError(t, err) + assert.Equal(t, int64(13), val) +} + +func TestIncrNonInteger(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "k1", []byte("not-a-number"), 0)) + + _, err := m.Incr(ctx, "c1", "k1") + require.Error(t, err) + assert.Contains(t, err.Error(), "not an integer") +} + +func TestIncrPreservesTTL(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "counter", []byte("5"), 1*time.Hour)) + + val, err := m.IncrBy(ctx, "c1", "counter", 3) + require.NoError(t, err) + assert.Equal(t, int64(8), val) + + ttl, err := m.GetTTL(ctx, "c1", "counter") + require.NoError(t, err) + assert.True(t, ttl > 0) +} diff --git a/providers/gcp/memorystore/memorystore.go b/providers/gcp/memorystore/memorystore.go index 63499428..88a3fca4 100644 --- a/providers/gcp/memorystore/memorystore.go +++ b/providers/gcp/memorystore/memorystore.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "path" + "strconv" "time" "github.com/stackshy/cloudemu/cache/driver" @@ -292,6 +293,136 @@ func (m *Mock) FlushAll(_ context.Context, cacheName string) error { return nil } +// Expire sets a TTL on an existing key. +func (m *Mock) Expire(_ context.Context, cacheName, key string, ttl time.Duration) error { + cd, ok := m.caches.Get(cacheName) + if !ok { + return errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + item, ok := cd.items.Get(key) + if !ok || (item.HasTTL && m.opts.Clock.Now().After(item.ExpiresAt)) { + return errors.Newf(errors.NotFound, "key %q not found in cache %q", key, cacheName) + } + + item.HasTTL = true + item.ExpiresAt = m.opts.Clock.Now().Add(ttl) + cd.items.Set(key, item) + + return nil +} + +// GetTTL returns the remaining TTL for a key. Returns -1 if the key has no TTL. +func (m *Mock) GetTTL(_ context.Context, cacheName, key string) (time.Duration, error) { + cd, ok := m.caches.Get(cacheName) + if !ok { + return 0, errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + item, ok := cd.items.Get(key) + if !ok || (item.HasTTL && m.opts.Clock.Now().After(item.ExpiresAt)) { + return 0, errors.Newf(errors.NotFound, "key %q not found in cache %q", key, cacheName) + } + + if !item.HasTTL { + return -1, nil + } + + return item.ExpiresAt.Sub(m.opts.Clock.Now()), nil +} + +// Persist removes the TTL from a key, making it persistent. +func (m *Mock) Persist(_ context.Context, cacheName, key string) error { + cd, ok := m.caches.Get(cacheName) + if !ok { + return errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + item, ok := cd.items.Get(key) + if !ok || (item.HasTTL && m.opts.Clock.Now().After(item.ExpiresAt)) { + return errors.Newf(errors.NotFound, "key %q not found in cache %q", key, cacheName) + } + + item.HasTTL = false + item.ExpiresAt = time.Time{} + cd.items.Set(key, item) + + return nil +} + +// Incr atomically increments the integer value of a key by 1. +func (m *Mock) Incr(ctx context.Context, cacheName, key string) (int64, error) { + return m.IncrBy(ctx, cacheName, key, 1) +} + +// IncrBy atomically increments the integer value of a key by delta. +func (m *Mock) IncrBy(ctx context.Context, cacheName, key string, delta int64) (int64, error) { + cd, ok := m.caches.Get(cacheName) + if !ok { + return 0, errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + newVal, err := applyDelta(cd, key, delta, m.opts.Clock.Now()) + if err != nil { + return 0, err + } + + m.emitMetric(ctx, "commands/total", 1, map[string]string{"instance_id": cacheName}) + + return newVal, nil +} + +// Decr atomically decrements the integer value of a key by 1. +func (m *Mock) Decr(ctx context.Context, cacheName, key string) (int64, error) { + return m.DecrBy(ctx, cacheName, key, 1) +} + +// DecrBy atomically decrements the integer value of a key by delta. +func (m *Mock) DecrBy(ctx context.Context, cacheName, key string, delta int64) (int64, error) { + cd, ok := m.caches.Get(cacheName) + if !ok { + return 0, errors.Newf(errors.NotFound, "cache %q not found", cacheName) + } + + newVal, err := applyDelta(cd, key, -delta, m.opts.Clock.Now()) + if err != nil { + return 0, err + } + + m.emitMetric(ctx, "commands/total", 1, map[string]string{"instance_id": cacheName}) + + return newVal, nil +} + +func applyDelta(cd *cacheData, key string, delta int64, now time.Time) (int64, error) { + item, ok := cd.items.Get(key) + + var current int64 + + if ok && (!item.HasTTL || !now.After(item.ExpiresAt)) { + val, err := strconv.ParseInt(string(item.Value), 10, 64) + if err != nil { + return 0, errors.New(errors.InvalidArgument, "value is not an integer") + } + + current = val + } + + newVal := current + delta + newItem := cacheItem{ + Value: []byte(strconv.FormatInt(newVal, 10)), + } + + if ok && item.HasTTL && !now.After(item.ExpiresAt) { + newItem.HasTTL = true + newItem.ExpiresAt = item.ExpiresAt + } + + cd.items.Set(key, newItem) + + return newVal, nil +} + // matchPattern matches a key against a glob-like pattern. // Supports full glob syntax including middle wildcards like "user:*:session". func matchPattern(pattern, key string) bool { diff --git a/providers/gcp/memorystore/memorystore_test.go b/providers/gcp/memorystore/memorystore_test.go index fb92ef91..fe032d2e 100644 --- a/providers/gcp/memorystore/memorystore_test.go +++ b/providers/gcp/memorystore/memorystore_test.go @@ -390,3 +390,123 @@ func TestMatchPattern(t *testing.T) { }) } } + +func TestExpire(t *testing.T) { + m, fc := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "k1", []byte("val"), 0)) + + ttl, err := m.GetTTL(ctx, "c1", "k1") + require.NoError(t, err) + assert.Equal(t, time.Duration(-1), ttl) + + require.NoError(t, m.Expire(ctx, "c1", "k1", 1*time.Hour)) + + ttl, err = m.GetTTL(ctx, "c1", "k1") + require.NoError(t, err) + assert.True(t, ttl > 0 && ttl <= 1*time.Hour) + + fc.Advance(2 * time.Hour) + + _, err = m.Get(ctx, "c1", "k1") + require.Error(t, err) +} + +func TestPersist(t *testing.T) { + m, fc := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "k1", []byte("val"), 1*time.Hour)) + require.NoError(t, m.Persist(ctx, "c1", "k1")) + + ttl, err := m.GetTTL(ctx, "c1", "k1") + require.NoError(t, err) + assert.Equal(t, time.Duration(-1), ttl) + + fc.Advance(2 * time.Hour) + + item, err := m.Get(ctx, "c1", "k1") + require.NoError(t, err) + assert.Equal(t, []byte("val"), item.Value) +} + +func TestIncr(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + val, err := m.Incr(ctx, "c1", "counter") + require.NoError(t, err) + assert.Equal(t, int64(1), val) + + val, err = m.Incr(ctx, "c1", "counter") + require.NoError(t, err) + assert.Equal(t, int64(2), val) +} + +func TestIncrBy(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "counter", []byte("10"), 0)) + + val, err := m.IncrBy(ctx, "c1", "counter", 5) + require.NoError(t, err) + assert.Equal(t, int64(15), val) +} + +func TestDecr(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "counter", []byte("10"), 0)) + + val, err := m.Decr(ctx, "c1", "counter") + require.NoError(t, err) + assert.Equal(t, int64(9), val) +} + +func TestDecrBy(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "counter", []byte("20"), 0)) + + val, err := m.DecrBy(ctx, "c1", "counter", 7) + require.NoError(t, err) + assert.Equal(t, int64(13), val) +} + +func TestIncrNonInteger(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "k1", []byte("not-a-number"), 0)) + + _, err := m.Incr(ctx, "c1", "k1") + require.Error(t, err) + assert.Contains(t, err.Error(), "not an integer") +} + +func TestIncrPreservesTTL(t *testing.T) { + m, _ := newTestMock() + ctx := context.Background() + createTestCache(t, m, "c1") + + require.NoError(t, m.Set(ctx, "c1", "counter", []byte("5"), 1*time.Hour)) + + val, err := m.IncrBy(ctx, "c1", "counter", 3) + require.NoError(t, err) + assert.Equal(t, int64(8), val) + + ttl, err := m.GetTTL(ctx, "c1", "counter") + require.NoError(t, err) + assert.True(t, ttl > 0) +} From 1dbf99a3dfbf3d6450ee96bcfa03a0359431e56a Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Tue, 31 Mar 2026 17:00:14 +0530 Subject: [PATCH 6/8] add bucket policy, CORS, and encryption config to storage service Closes #59 --- cloudemu_test.go | 235 +++++++++++++++++++++ providers/aws/s3/s3.go | 114 ++++++++++ providers/azure/blobstorage/blobstorage.go | 106 ++++++++++ providers/gcp/gcs/gcs.go | 106 ++++++++++ storage/driver/driver.go | 49 +++++ storage/storage.go | 81 +++++++ 6 files changed, 691 insertions(+) diff --git a/cloudemu_test.go b/cloudemu_test.go index 37f074b5..e2a4ea08 100644 --- a/cloudemu_test.go +++ b/cloudemu_test.go @@ -6244,3 +6244,238 @@ func TestCacheIncrDecrGCP(t *testing.T) { t.Errorf("expected 5, got %d", val) } } + +func TestBucketPolicyAWS(t *testing.T) { + ctx := context.Background() + p := NewAWS() + + if err := p.S3.CreateBucket(ctx, "b1"); err != nil { + t.Fatal(err) + } + + policy := storagedriver.BucketPolicy{ + Version: "2012-10-17", + Statements: []storagedriver.PolicyStatement{ + {Effect: "Allow", Principal: "*", Actions: []string{"s3:GetObject"}, Resources: []string{"arn:aws:s3:::b1/*"}}, + }, + } + + if err := p.S3.PutBucketPolicy(ctx, "b1", policy); err != nil { + t.Fatal(err) + } + + got, err := p.S3.GetBucketPolicy(ctx, "b1") + if err != nil { + t.Fatal(err) + } + + if got.Version != "2012-10-17" { + t.Errorf("expected version '2012-10-17', got %q", got.Version) + } + + if len(got.Statements) != 1 { + t.Fatalf("expected 1 statement, got %d", len(got.Statements)) + } + + if got.Statements[0].Effect != "Allow" { + t.Errorf("expected effect 'Allow', got %q", got.Statements[0].Effect) + } + + if err := p.S3.DeleteBucketPolicy(ctx, "b1"); err != nil { + t.Fatal(err) + } + + _, err = p.S3.GetBucketPolicy(ctx, "b1") + if !cerrors.IsNotFound(err) { + t.Errorf("expected NotFound after delete, got %v", err) + } +} + +func TestBucketPolicyAzure(t *testing.T) { + ctx := context.Background() + p := NewAzure() + + if err := p.BlobStorage.CreateBucket(ctx, "c1"); err != nil { + t.Fatal(err) + } + + policy := storagedriver.BucketPolicy{ + Version: "1.0", + Statements: []storagedriver.PolicyStatement{ + {Effect: "Allow", Principal: "*", Actions: []string{"read"}, Resources: []string{"c1/*"}}, + }, + } + + if err := p.BlobStorage.PutBucketPolicy(ctx, "c1", policy); err != nil { + t.Fatal(err) + } + + got, err := p.BlobStorage.GetBucketPolicy(ctx, "c1") + if err != nil { + t.Fatal(err) + } + + if len(got.Statements) != 1 { + t.Fatalf("expected 1 statement, got %d", len(got.Statements)) + } +} + +func TestBucketPolicyGCP(t *testing.T) { + ctx := context.Background() + p := NewGCP() + + if err := p.GCS.CreateBucket(ctx, "b1"); err != nil { + t.Fatal(err) + } + + policy := storagedriver.BucketPolicy{ + Version: "1", + Statements: []storagedriver.PolicyStatement{ + {Effect: "Allow", Principal: "allUsers", Actions: []string{"storage.objects.get"}, Resources: []string{"b1/*"}}, + }, + } + + if err := p.GCS.PutBucketPolicy(ctx, "b1", policy); err != nil { + t.Fatal(err) + } + + got, err := p.GCS.GetBucketPolicy(ctx, "b1") + if err != nil { + t.Fatal(err) + } + + if len(got.Statements) != 1 { + t.Fatalf("expected 1 statement, got %d", len(got.Statements)) + } +} + +func TestCORSConfigAWS(t *testing.T) { + ctx := context.Background() + p := NewAWS() + + if err := p.S3.CreateBucket(ctx, "b1"); err != nil { + t.Fatal(err) + } + + cors := storagedriver.CORSConfig{ + Rules: []storagedriver.CORSRule{ + { + AllowedOrigins: []string{"https://example.com"}, + AllowedMethods: []string{"GET", "PUT"}, + AllowedHeaders: []string{"*"}, + MaxAgeSeconds: 3600, + }, + }, + } + + if err := p.S3.PutCORSConfig(ctx, "b1", cors); err != nil { + t.Fatal(err) + } + + got, err := p.S3.GetCORSConfig(ctx, "b1") + if err != nil { + t.Fatal(err) + } + + if len(got.Rules) != 1 { + t.Fatalf("expected 1 CORS rule, got %d", len(got.Rules)) + } + + if got.Rules[0].AllowedOrigins[0] != "https://example.com" { + t.Errorf("expected origin 'https://example.com', got %q", got.Rules[0].AllowedOrigins[0]) + } + + if err := p.S3.DeleteCORSConfig(ctx, "b1"); err != nil { + t.Fatal(err) + } + + _, err = p.S3.GetCORSConfig(ctx, "b1") + if !cerrors.IsNotFound(err) { + t.Errorf("expected NotFound after delete, got %v", err) + } +} + +func TestEncryptionConfigAWS(t *testing.T) { + ctx := context.Background() + p := NewAWS() + + if err := p.S3.CreateBucket(ctx, "b1"); err != nil { + t.Fatal(err) + } + + enc := storagedriver.EncryptionConfig{ + Enabled: true, + Algorithm: "AES256", + } + + if err := p.S3.PutEncryptionConfig(ctx, "b1", enc); err != nil { + t.Fatal(err) + } + + got, err := p.S3.GetEncryptionConfig(ctx, "b1") + if err != nil { + t.Fatal(err) + } + + if !got.Enabled { + t.Error("expected encryption enabled") + } + + if got.Algorithm != "AES256" { + t.Errorf("expected algorithm 'AES256', got %q", got.Algorithm) + } +} + +func TestEncryptionConfigAzure(t *testing.T) { + ctx := context.Background() + p := NewAzure() + + if err := p.BlobStorage.CreateBucket(ctx, "c1"); err != nil { + t.Fatal(err) + } + + enc := storagedriver.EncryptionConfig{ + Enabled: true, + Algorithm: "AES256", + } + + if err := p.BlobStorage.PutEncryptionConfig(ctx, "c1", enc); err != nil { + t.Fatal(err) + } + + got, err := p.BlobStorage.GetEncryptionConfig(ctx, "c1") + if err != nil { + t.Fatal(err) + } + + if !got.Enabled { + t.Error("expected encryption enabled") + } +} + +func TestEncryptionConfigGCP(t *testing.T) { + ctx := context.Background() + p := NewGCP() + + if err := p.GCS.CreateBucket(ctx, "b1"); err != nil { + t.Fatal(err) + } + + enc := storagedriver.EncryptionConfig{ + Enabled: true, + Algorithm: "AES256", + } + + if err := p.GCS.PutEncryptionConfig(ctx, "b1", enc); err != nil { + t.Fatal(err) + } + + got, err := p.GCS.GetEncryptionConfig(ctx, "b1") + if err != nil { + t.Fatal(err) + } + + if !got.Enabled { + t.Error("expected encryption enabled") + } +} diff --git a/providers/aws/s3/s3.go b/providers/aws/s3/s3.go index 115558cf..592f39d8 100644 --- a/providers/aws/s3/s3.go +++ b/providers/aws/s3/s3.go @@ -53,6 +53,9 @@ type bucketMeta struct { lifecycle *driver.LifecycleConfig multiparts *memstore.Store[*multipartUpload] versioning bool + policy *driver.BucketPolicy + corsConfig *driver.CORSConfig + encryption *driver.EncryptionConfig } // Mock is an in-memory mock implementation of the AWS S3 service. @@ -604,3 +607,114 @@ func (m *Mock) GetBucketVersioning(_ context.Context, bucket string) (bool, erro return bkt.versioning, nil } + +// PutBucketPolicy sets the bucket policy. +func (m *Mock) PutBucketPolicy(_ context.Context, bucket string, policy driver.BucketPolicy) error { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + p := policy + bkt.policy = &p + + return nil +} + +// GetBucketPolicy returns the bucket policy. +func (m *Mock) GetBucketPolicy(_ context.Context, bucket string) (*driver.BucketPolicy, error) { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return nil, cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + if bkt.policy == nil { + return nil, cerrors.Newf(cerrors.NotFound, "no policy set for bucket %q", bucket) + } + + p := *bkt.policy + + return &p, nil +} + +// DeleteBucketPolicy removes the bucket policy. +func (m *Mock) DeleteBucketPolicy(_ context.Context, bucket string) error { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + bkt.policy = nil + + return nil +} + +// PutCORSConfig sets the CORS configuration for a bucket. +func (m *Mock) PutCORSConfig(_ context.Context, bucket string, cfg driver.CORSConfig) error { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + c := cfg + bkt.corsConfig = &c + + return nil +} + +// GetCORSConfig returns the CORS configuration for a bucket. +func (m *Mock) GetCORSConfig(_ context.Context, bucket string) (*driver.CORSConfig, error) { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return nil, cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + if bkt.corsConfig == nil { + return nil, cerrors.Newf(cerrors.NotFound, "no CORS config set for bucket %q", bucket) + } + + c := *bkt.corsConfig + + return &c, nil +} + +// DeleteCORSConfig removes the CORS configuration for a bucket. +func (m *Mock) DeleteCORSConfig(_ context.Context, bucket string) error { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + bkt.corsConfig = nil + + return nil +} + +// PutEncryptionConfig sets the default encryption for a bucket. +func (m *Mock) PutEncryptionConfig(_ context.Context, bucket string, cfg driver.EncryptionConfig) error { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + e := cfg + bkt.encryption = &e + + return nil +} + +// GetEncryptionConfig returns the default encryption for a bucket. +func (m *Mock) GetEncryptionConfig(_ context.Context, bucket string) (*driver.EncryptionConfig, error) { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return nil, cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + if bkt.encryption == nil { + return nil, cerrors.Newf(cerrors.NotFound, "no encryption config set for bucket %q", bucket) + } + + e := *bkt.encryption + + return &e, nil +} diff --git a/providers/azure/blobstorage/blobstorage.go b/providers/azure/blobstorage/blobstorage.go index 2fb7566c..5a4e03ea 100644 --- a/providers/azure/blobstorage/blobstorage.go +++ b/providers/azure/blobstorage/blobstorage.go @@ -55,6 +55,9 @@ type containerMeta struct { lifecycle *driver.LifecycleConfig multiparts *memstore.Store[*blobMultipartUpload] versioning bool + policy *driver.BucketPolicy + corsConfig *driver.CORSConfig + encryption *driver.EncryptionConfig } // Mock is an in-memory mock implementation of Azure Blob Storage. @@ -619,3 +622,106 @@ func (m *Mock) GetBucketVersioning(_ context.Context, bucket string) (bool, erro return ctr.versioning, nil } + +func (m *Mock) PutBucketPolicy(_ context.Context, bucket string, policy driver.BucketPolicy) error { + ctr, ok := m.containers.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "container %q not found", bucket) + } + + p := policy + ctr.policy = &p + + return nil +} + +func (m *Mock) GetBucketPolicy(_ context.Context, bucket string) (*driver.BucketPolicy, error) { + ctr, ok := m.containers.Get(bucket) + if !ok { + return nil, cerrors.Newf(cerrors.NotFound, "container %q not found", bucket) + } + + if ctr.policy == nil { + return nil, cerrors.Newf(cerrors.NotFound, "no policy set for container %q", bucket) + } + + p := *ctr.policy + + return &p, nil +} + +func (m *Mock) DeleteBucketPolicy(_ context.Context, bucket string) error { + ctr, ok := m.containers.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "container %q not found", bucket) + } + + ctr.policy = nil + + return nil +} + +func (m *Mock) PutCORSConfig(_ context.Context, bucket string, cfg driver.CORSConfig) error { + ctr, ok := m.containers.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "container %q not found", bucket) + } + + c := cfg + ctr.corsConfig = &c + + return nil +} + +func (m *Mock) GetCORSConfig(_ context.Context, bucket string) (*driver.CORSConfig, error) { + ctr, ok := m.containers.Get(bucket) + if !ok { + return nil, cerrors.Newf(cerrors.NotFound, "container %q not found", bucket) + } + + if ctr.corsConfig == nil { + return nil, cerrors.Newf(cerrors.NotFound, "no CORS config set for container %q", bucket) + } + + c := *ctr.corsConfig + + return &c, nil +} + +func (m *Mock) DeleteCORSConfig(_ context.Context, bucket string) error { + ctr, ok := m.containers.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "container %q not found", bucket) + } + + ctr.corsConfig = nil + + return nil +} + +func (m *Mock) PutEncryptionConfig(_ context.Context, bucket string, cfg driver.EncryptionConfig) error { + ctr, ok := m.containers.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "container %q not found", bucket) + } + + e := cfg + ctr.encryption = &e + + return nil +} + +func (m *Mock) GetEncryptionConfig(_ context.Context, bucket string) (*driver.EncryptionConfig, error) { + ctr, ok := m.containers.Get(bucket) + if !ok { + return nil, cerrors.Newf(cerrors.NotFound, "container %q not found", bucket) + } + + if ctr.encryption == nil { + return nil, cerrors.Newf(cerrors.NotFound, "no encryption config set for container %q", bucket) + } + + e := *ctr.encryption + + return &e, nil +} diff --git a/providers/gcp/gcs/gcs.go b/providers/gcp/gcs/gcs.go index b1688b6d..247068f9 100644 --- a/providers/gcp/gcs/gcs.go +++ b/providers/gcp/gcs/gcs.go @@ -55,6 +55,9 @@ type bucketMeta struct { lifecycle *driver.LifecycleConfig multiparts *memstore.Store[*gcsMultipartUpload] versioning bool + policy *driver.BucketPolicy + corsConfig *driver.CORSConfig + encryption *driver.EncryptionConfig } // Mock is an in-memory mock implementation of Google Cloud Storage. @@ -616,3 +619,106 @@ func (m *Mock) GetBucketVersioning(_ context.Context, bucket string) (bool, erro return bkt.versioning, nil } + +func (m *Mock) PutBucketPolicy(_ context.Context, bucket string, policy driver.BucketPolicy) error { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + p := policy + bkt.policy = &p + + return nil +} + +func (m *Mock) GetBucketPolicy(_ context.Context, bucket string) (*driver.BucketPolicy, error) { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return nil, cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + if bkt.policy == nil { + return nil, cerrors.Newf(cerrors.NotFound, "no policy set for bucket %q", bucket) + } + + p := *bkt.policy + + return &p, nil +} + +func (m *Mock) DeleteBucketPolicy(_ context.Context, bucket string) error { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + bkt.policy = nil + + return nil +} + +func (m *Mock) PutCORSConfig(_ context.Context, bucket string, cfg driver.CORSConfig) error { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + c := cfg + bkt.corsConfig = &c + + return nil +} + +func (m *Mock) GetCORSConfig(_ context.Context, bucket string) (*driver.CORSConfig, error) { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return nil, cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + if bkt.corsConfig == nil { + return nil, cerrors.Newf(cerrors.NotFound, "no CORS config set for bucket %q", bucket) + } + + c := *bkt.corsConfig + + return &c, nil +} + +func (m *Mock) DeleteCORSConfig(_ context.Context, bucket string) error { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + bkt.corsConfig = nil + + return nil +} + +func (m *Mock) PutEncryptionConfig(_ context.Context, bucket string, cfg driver.EncryptionConfig) error { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + e := cfg + bkt.encryption = &e + + return nil +} + +func (m *Mock) GetEncryptionConfig(_ context.Context, bucket string) (*driver.EncryptionConfig, error) { + bkt, ok := m.buckets.Get(bucket) + if !ok { + return nil, cerrors.Newf(cerrors.NotFound, "bucket %q not found", bucket) + } + + if bkt.encryption == nil { + return nil, cerrors.Newf(cerrors.NotFound, "no encryption config set for bucket %q", bucket) + } + + e := *bkt.encryption + + return &e, nil +} diff --git a/storage/driver/driver.go b/storage/driver/driver.go index 02b77116..f5c77869 100644 --- a/storage/driver/driver.go +++ b/storage/driver/driver.go @@ -98,6 +98,41 @@ type UploadPart struct { Size int64 } +// BucketPolicy represents a bucket access policy. +type BucketPolicy struct { + Version string + Statements []PolicyStatement +} + +// PolicyStatement represents a single statement in a bucket policy. +type PolicyStatement struct { + Effect string // "Allow" or "Deny" + Principal string // "*" or specific principal + Actions []string // e.g., "s3:GetObject" + Resources []string // e.g., "arn:aws:s3:::bucket/*" +} + +// CORSRule defines a CORS rule for a bucket. +type CORSRule struct { + AllowedOrigins []string + AllowedMethods []string + AllowedHeaders []string + ExposeHeaders []string + MaxAgeSeconds int +} + +// CORSConfig is a set of CORS rules for a bucket. +type CORSConfig struct { + Rules []CORSRule +} + +// EncryptionConfig describes the default encryption for a bucket. +type EncryptionConfig struct { + Enabled bool + Algorithm string // "AES256" or "aws:kms" + KeyID string // KMS key ID (optional) +} + // Bucket is the interface that storage provider implementations must satisfy. type Bucket interface { CreateBucket(ctx context.Context, name string) error @@ -129,4 +164,18 @@ type Bucket interface { // Versioning SetBucketVersioning(ctx context.Context, bucket string, enabled bool) error GetBucketVersioning(ctx context.Context, bucket string) (bool, error) + + // Bucket Policy + PutBucketPolicy(ctx context.Context, bucket string, policy BucketPolicy) error + GetBucketPolicy(ctx context.Context, bucket string) (*BucketPolicy, error) + DeleteBucketPolicy(ctx context.Context, bucket string) error + + // CORS + PutCORSConfig(ctx context.Context, bucket string, config CORSConfig) error + GetCORSConfig(ctx context.Context, bucket string) (*CORSConfig, error) + DeleteCORSConfig(ctx context.Context, bucket string) error + + // Encryption + PutEncryptionConfig(ctx context.Context, bucket string, config EncryptionConfig) error + GetEncryptionConfig(ctx context.Context, bucket string) (*EncryptionConfig, error) } diff --git a/storage/storage.go b/storage/storage.go index 5e3febc4..a58f17a6 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -318,3 +318,84 @@ func (b *Bucket) GetBucketVersioning(ctx context.Context, bucket string) (bool, return out.(bool), nil } + +// PutBucketPolicy sets the bucket policy. +func (b *Bucket) PutBucketPolicy(ctx context.Context, bucket string, policy driver.BucketPolicy) error { + _, err := b.do(ctx, "PutBucketPolicy", bucket, func() (any, error) { + return nil, b.driver.PutBucketPolicy(ctx, bucket, policy) + }) + + return err +} + +// GetBucketPolicy returns the bucket policy. +func (b *Bucket) GetBucketPolicy(ctx context.Context, bucket string) (*driver.BucketPolicy, error) { + out, err := b.do(ctx, "GetBucketPolicy", bucket, func() (any, error) { + return b.driver.GetBucketPolicy(ctx, bucket) + }) + if err != nil { + return nil, err + } + + return out.(*driver.BucketPolicy), nil +} + +// DeleteBucketPolicy removes the bucket policy. +func (b *Bucket) DeleteBucketPolicy(ctx context.Context, bucket string) error { + _, err := b.do(ctx, "DeleteBucketPolicy", bucket, func() (any, error) { + return nil, b.driver.DeleteBucketPolicy(ctx, bucket) + }) + + return err +} + +// PutCORSConfig sets the CORS configuration for a bucket. +func (b *Bucket) PutCORSConfig(ctx context.Context, bucket string, cfg driver.CORSConfig) error { + _, err := b.do(ctx, "PutCORSConfig", bucket, func() (any, error) { + return nil, b.driver.PutCORSConfig(ctx, bucket, cfg) + }) + + return err +} + +// GetCORSConfig returns the CORS configuration for a bucket. +func (b *Bucket) GetCORSConfig(ctx context.Context, bucket string) (*driver.CORSConfig, error) { + out, err := b.do(ctx, "GetCORSConfig", bucket, func() (any, error) { + return b.driver.GetCORSConfig(ctx, bucket) + }) + if err != nil { + return nil, err + } + + return out.(*driver.CORSConfig), nil +} + +// DeleteCORSConfig removes the CORS configuration for a bucket. +func (b *Bucket) DeleteCORSConfig(ctx context.Context, bucket string) error { + _, err := b.do(ctx, "DeleteCORSConfig", bucket, func() (any, error) { + return nil, b.driver.DeleteCORSConfig(ctx, bucket) + }) + + return err +} + +// PutEncryptionConfig sets the default encryption for a bucket. +func (b *Bucket) PutEncryptionConfig(ctx context.Context, bucket string, cfg driver.EncryptionConfig) error { + _, err := b.do(ctx, "PutEncryptionConfig", bucket, func() (any, error) { + return nil, b.driver.PutEncryptionConfig(ctx, bucket, cfg) + }) + + return err +} + +// GetEncryptionConfig returns the default encryption for a bucket. +func (b *Bucket) GetEncryptionConfig(ctx context.Context, bucket string) (*driver.EncryptionConfig, error) { + out, err := b.do(ctx, "GetEncryptionConfig", bucket, func() (any, error) { + return b.driver.GetEncryptionConfig(ctx, bucket) + }) + if err != nil { + return nil, err + } + + return out.(*driver.EncryptionConfig), nil +} From 03df44ddbbc2bd4d4daf6d1756793193162f68cb Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Wed, 1 Apr 2026 00:41:42 +0530 Subject: [PATCH 7/8] add listener rules, modify listener, and LB attributes to load balancer service Closes #63 --- cloudemu_test.go | 304 +++++++++++++++++++++++++++++ loadbalancer/driver/driver.go | 55 ++++++ loadbalancer/loadbalancer.go | 42 ++++ loadbalancer/loadbalancer_test.go | 81 ++++++++ providers/aws/elb/elb.go | 131 ++++++++++++- providers/aws/elb/elb_test.go | 151 ++++++++++++++ providers/azure/azurelb/lb.go | 131 ++++++++++++- providers/azure/azurelb/lb_test.go | 157 +++++++++++++++ providers/gcp/gcplb/lb.go | 130 +++++++++++- providers/gcp/gcplb/lb_test.go | 173 ++++++++++++++++ 10 files changed, 1337 insertions(+), 18 deletions(-) diff --git a/cloudemu_test.go b/cloudemu_test.go index e2a4ea08..bbb81c88 100644 --- a/cloudemu_test.go +++ b/cloudemu_test.go @@ -26,6 +26,8 @@ import ( "github.com/stackshy/cloudemu/storage" storagedriver "github.com/stackshy/cloudemu/storage/driver" + lbdriver "github.com/stackshy/cloudemu/loadbalancer/driver" + cachedriver "github.com/stackshy/cloudemu/cache/driver" crdriver "github.com/stackshy/cloudemu/containerregistry/driver" ebdriver "github.com/stackshy/cloudemu/eventbus/driver" @@ -6479,3 +6481,305 @@ func TestEncryptionConfigGCP(t *testing.T) { t.Error("expected encryption enabled") } } + +func TestListenerRulesAWS(t *testing.T) { + ctx := context.Background() + p := NewAWS() + + lb, err := p.ELB.CreateLoadBalancer(ctx, lbdriver.LBConfig{ + Name: "test-lb", Type: "application", Scheme: "internet-facing", + }) + if err != nil { + t.Fatal(err) + } + + tg, err := p.ELB.CreateTargetGroup(ctx, lbdriver.TargetGroupConfig{ + Name: "test-tg", Protocol: "HTTP", Port: 80, VPCID: "vpc-1", + }) + if err != nil { + t.Fatal(err) + } + + li, err := p.ELB.CreateListener(ctx, lbdriver.ListenerConfig{ + LBARN: lb.ARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tg.ARN, + }) + if err != nil { + t.Fatal(err) + } + + // Create rules with path conditions + rule1, err := p.ELB.CreateRule(ctx, lbdriver.RuleConfig{ + ListenerARN: li.ARN, + Priority: 10, + Conditions: []lbdriver.RuleCondition{{Field: "path-pattern", Values: []string{"/api/*"}}}, + Actions: []lbdriver.RuleAction{{Type: "forward", TargetGroupARN: tg.ARN}}, + }) + if err != nil { + t.Fatal(err) + } + + if rule1.ARN == "" { + t.Error("expected non-empty rule ARN") + } + + if rule1.Priority != 10 { + t.Errorf("expected priority 10, got %d", rule1.Priority) + } + + _, err = p.ELB.CreateRule(ctx, lbdriver.RuleConfig{ + ListenerARN: li.ARN, + Priority: 20, + Conditions: []lbdriver.RuleCondition{{Field: "host-header", Values: []string{"example.com"}}}, + Actions: []lbdriver.RuleAction{{Type: "forward", TargetGroupARN: tg.ARN}}, + }) + if err != nil { + t.Fatal(err) + } + + // Describe rules + rules, err := p.ELB.DescribeRules(ctx, li.ARN) + if err != nil { + t.Fatal(err) + } + + if len(rules) != 2 { + t.Errorf("expected 2 rules, got %d", len(rules)) + } + + // Delete a rule + if err := p.ELB.DeleteRule(ctx, rule1.ARN); err != nil { + t.Fatal(err) + } + + rules, err = p.ELB.DescribeRules(ctx, li.ARN) + if err != nil { + t.Fatal(err) + } + + if len(rules) != 1 { + t.Errorf("expected 1 rule after deletion, got %d", len(rules)) + } +} + +func TestModifyListenerAWS(t *testing.T) { + ctx := context.Background() + p := NewAWS() + + lb, err := p.ELB.CreateLoadBalancer(ctx, lbdriver.LBConfig{ + Name: "test-lb", Type: "application", Scheme: "internet-facing", + }) + if err != nil { + t.Fatal(err) + } + + tg, err := p.ELB.CreateTargetGroup(ctx, lbdriver.TargetGroupConfig{ + Name: "test-tg", Protocol: "HTTP", Port: 80, VPCID: "vpc-1", + }) + if err != nil { + t.Fatal(err) + } + + li, err := p.ELB.CreateListener(ctx, lbdriver.ListenerConfig{ + LBARN: lb.ARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tg.ARN, + }) + if err != nil { + t.Fatal(err) + } + + // Modify port + if err := p.ELB.ModifyListener(ctx, lbdriver.ModifyListenerInput{ + ListenerARN: li.ARN, Port: 8080, + }); err != nil { + t.Fatal(err) + } + + listeners, err := p.ELB.DescribeListeners(ctx, lb.ARN) + if err != nil { + t.Fatal(err) + } + + if len(listeners) != 1 { + t.Fatalf("expected 1 listener, got %d", len(listeners)) + } + + if listeners[0].Port != 8080 { + t.Errorf("expected port 8080, got %d", listeners[0].Port) + } +} + +func TestLBAttributesAWS(t *testing.T) { + ctx := context.Background() + p := NewAWS() + + lb, err := p.ELB.CreateLoadBalancer(ctx, lbdriver.LBConfig{ + Name: "test-lb", Type: "application", Scheme: "internet-facing", + }) + if err != nil { + t.Fatal(err) + } + + // Get default attributes + attrs, err := p.ELB.GetLBAttributes(ctx, lb.ARN) + if err != nil { + t.Fatal(err) + } + + if attrs.IdleTimeout != 60 { + t.Errorf("expected default idle timeout 60, got %d", attrs.IdleTimeout) + } + + // Put custom attributes + if err := p.ELB.PutLBAttributes(ctx, lb.ARN, lbdriver.LBAttributes{ + IdleTimeout: 120, + DeletionProtection: true, + AccessLogsEnabled: true, + AccessLogsBucket: "my-access-logs", + }); err != nil { + t.Fatal(err) + } + + attrs, err = p.ELB.GetLBAttributes(ctx, lb.ARN) + if err != nil { + t.Fatal(err) + } + + if attrs.IdleTimeout != 120 { + t.Errorf("expected idle timeout 120, got %d", attrs.IdleTimeout) + } + + if !attrs.DeletionProtection { + t.Error("expected deletion protection enabled") + } + + if !attrs.AccessLogsEnabled { + t.Error("expected access logs enabled") + } + + if attrs.AccessLogsBucket != "my-access-logs" { + t.Errorf("expected bucket 'my-access-logs', got %q", attrs.AccessLogsBucket) + } +} + +func TestListenerRulesAzure(t *testing.T) { + ctx := context.Background() + p := NewAzure() + + lb, err := p.LB.CreateLoadBalancer(ctx, lbdriver.LBConfig{ + Name: "test-lb", Type: "application", Scheme: "internet-facing", + }) + if err != nil { + t.Fatal(err) + } + + tg, err := p.LB.CreateTargetGroup(ctx, lbdriver.TargetGroupConfig{ + Name: "test-tg", Protocol: "HTTP", Port: 80, VPCID: "vnet-1", + }) + if err != nil { + t.Fatal(err) + } + + li, err := p.LB.CreateListener(ctx, lbdriver.ListenerConfig{ + LBARN: lb.ARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tg.ARN, + }) + if err != nil { + t.Fatal(err) + } + + rule, err := p.LB.CreateRule(ctx, lbdriver.RuleConfig{ + ListenerARN: li.ARN, + Priority: 10, + Conditions: []lbdriver.RuleCondition{{Field: "path-pattern", Values: []string{"/api/*"}}}, + Actions: []lbdriver.RuleAction{{Type: "forward", TargetGroupARN: tg.ARN}}, + }) + if err != nil { + t.Fatal(err) + } + + if rule.ARN == "" { + t.Error("expected non-empty rule ARN") + } + + rules, err := p.LB.DescribeRules(ctx, li.ARN) + if err != nil { + t.Fatal(err) + } + + if len(rules) != 1 { + t.Errorf("expected 1 rule, got %d", len(rules)) + } + + if err := p.LB.DeleteRule(ctx, rule.ARN); err != nil { + t.Fatal(err) + } + + rules, err = p.LB.DescribeRules(ctx, li.ARN) + if err != nil { + t.Fatal(err) + } + + if len(rules) != 0 { + t.Errorf("expected 0 rules after deletion, got %d", len(rules)) + } +} + +func TestListenerRulesGCP(t *testing.T) { + ctx := context.Background() + p := NewGCP() + + lb, err := p.LB.CreateLoadBalancer(ctx, lbdriver.LBConfig{ + Name: "test-lb", Type: "application", Scheme: "internet-facing", + }) + if err != nil { + t.Fatal(err) + } + + tg, err := p.LB.CreateTargetGroup(ctx, lbdriver.TargetGroupConfig{ + Name: "test-tg", Protocol: "HTTP", Port: 80, VPCID: "vpc-1", + }) + if err != nil { + t.Fatal(err) + } + + li, err := p.LB.CreateListener(ctx, lbdriver.ListenerConfig{ + LBARN: lb.ARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tg.ARN, + }) + if err != nil { + t.Fatal(err) + } + + rule, err := p.LB.CreateRule(ctx, lbdriver.RuleConfig{ + ListenerARN: li.ARN, + Priority: 10, + Conditions: []lbdriver.RuleCondition{{Field: "path-pattern", Values: []string{"/api/*"}}}, + Actions: []lbdriver.RuleAction{{Type: "forward", TargetGroupARN: tg.ARN}}, + }) + if err != nil { + t.Fatal(err) + } + + if rule.ARN == "" { + t.Error("expected non-empty rule ARN") + } + + rules, err := p.LB.DescribeRules(ctx, li.ARN) + if err != nil { + t.Fatal(err) + } + + if len(rules) != 1 { + t.Errorf("expected 1 rule, got %d", len(rules)) + } + + if err := p.LB.DeleteRule(ctx, rule.ARN); err != nil { + t.Fatal(err) + } + + rules, err = p.LB.DescribeRules(ctx, li.ARN) + if err != nil { + t.Fatal(err) + } + + if len(rules) != 0 { + t.Errorf("expected 0 rules after deletion, got %d", len(rules)) + } +} diff --git a/loadbalancer/driver/driver.go b/loadbalancer/driver/driver.go index 3c0b8f2c..2543e949 100644 --- a/loadbalancer/driver/driver.go +++ b/loadbalancer/driver/driver.go @@ -64,6 +64,52 @@ type ListenerInfo struct { TargetGroupARN string } +// RuleCondition describes a condition for a listener rule (e.g., path-pattern or host-header). +type RuleCondition struct { + Field string // "path-pattern" or "host-header" + Values []string +} + +// RuleAction describes an action for a listener rule (e.g., forward to a target group). +type RuleAction struct { + Type string // "forward" + TargetGroupARN string +} + +// RuleConfig describes a listener rule to create. +type RuleConfig struct { + ListenerARN string + Priority int + Conditions []RuleCondition + Actions []RuleAction +} + +// RuleInfo describes a listener rule. +type RuleInfo struct { + ARN string + ListenerARN string + Priority int + Conditions []RuleCondition + Actions []RuleAction + IsDefault bool +} + +// ModifyListenerInput describes modifications to apply to a listener. +type ModifyListenerInput struct { + ListenerARN string + Port int + Protocol string + DefaultActions []RuleAction +} + +// LBAttributes describes configurable attributes of a load balancer. +type LBAttributes struct { + IdleTimeout int + DeletionProtection bool + AccessLogsEnabled bool + AccessLogsBucket string +} + // Target identifies a target (e.g., instance) in a target group. type Target struct { ID string @@ -91,6 +137,15 @@ type LoadBalancer interface { DeleteListener(ctx context.Context, arn string) error DescribeListeners(ctx context.Context, lbARN string) ([]ListenerInfo, error) + CreateRule(ctx context.Context, config RuleConfig) (*RuleInfo, error) + DeleteRule(ctx context.Context, ruleARN string) error + DescribeRules(ctx context.Context, listenerARN string) ([]RuleInfo, error) + + ModifyListener(ctx context.Context, input ModifyListenerInput) error + + GetLBAttributes(ctx context.Context, lbARN string) (*LBAttributes, error) + PutLBAttributes(ctx context.Context, lbARN string, attrs LBAttributes) error + RegisterTargets(ctx context.Context, targetGroupARN string, targets []Target) error DeregisterTargets(ctx context.Context, targetGroupARN string, targets []Target) error DescribeTargetHealth(ctx context.Context, targetGroupARN string) ([]TargetHealth, error) diff --git a/loadbalancer/loadbalancer.go b/loadbalancer/loadbalancer.go index 46ce862b..c5fcd9fa 100644 --- a/loadbalancer/loadbalancer.go +++ b/loadbalancer/loadbalancer.go @@ -154,6 +154,48 @@ func (lb *LB) DescribeListeners(ctx context.Context, lbARN string) ([]driver.Lis return out.([]driver.ListenerInfo), nil } +func (lb *LB) CreateRule(ctx context.Context, config driver.RuleConfig) (*driver.RuleInfo, error) { + out, err := lb.do(ctx, "CreateRule", config, func() (any, error) { return lb.driver.CreateRule(ctx, config) }) + if err != nil { + return nil, err + } + + return out.(*driver.RuleInfo), nil +} + +func (lb *LB) DeleteRule(ctx context.Context, ruleARN string) error { + _, err := lb.do(ctx, "DeleteRule", ruleARN, func() (any, error) { return nil, lb.driver.DeleteRule(ctx, ruleARN) }) + return err +} + +func (lb *LB) DescribeRules(ctx context.Context, listenerARN string) ([]driver.RuleInfo, error) { + out, err := lb.do(ctx, "DescribeRules", listenerARN, func() (any, error) { return lb.driver.DescribeRules(ctx, listenerARN) }) + if err != nil { + return nil, err + } + + return out.([]driver.RuleInfo), nil +} + +func (lb *LB) ModifyListener(ctx context.Context, input driver.ModifyListenerInput) error { + _, err := lb.do(ctx, "ModifyListener", input, func() (any, error) { return nil, lb.driver.ModifyListener(ctx, input) }) + return err +} + +func (lb *LB) GetLBAttributes(ctx context.Context, lbARN string) (*driver.LBAttributes, error) { + out, err := lb.do(ctx, "GetLBAttributes", lbARN, func() (any, error) { return lb.driver.GetLBAttributes(ctx, lbARN) }) + if err != nil { + return nil, err + } + + return out.(*driver.LBAttributes), nil +} + +func (lb *LB) PutLBAttributes(ctx context.Context, lbARN string, attrs driver.LBAttributes) error { + _, err := lb.do(ctx, "PutLBAttributes", lbARN, func() (any, error) { return nil, lb.driver.PutLBAttributes(ctx, lbARN, attrs) }) + return err +} + func (lb *LB) RegisterTargets(ctx context.Context, tgARN string, targets []driver.Target) error { _, err := lb.do(ctx, "RegisterTargets", tgARN, func() (any, error) { return nil, lb.driver.RegisterTargets(ctx, tgARN, targets) }) return err diff --git a/loadbalancer/loadbalancer_test.go b/loadbalancer/loadbalancer_test.go index d40e9758..e5a0c000 100644 --- a/loadbalancer/loadbalancer_test.go +++ b/loadbalancer/loadbalancer_test.go @@ -19,7 +19,9 @@ type mockDriver struct { lbs map[string]*driver.LBInfo targetGroups map[string]*driver.TargetGroupInfo listeners map[string]*driver.ListenerInfo + rules map[string]*driver.RuleInfo targets map[string][]driver.TargetHealth + attrs map[string]driver.LBAttributes seq int } @@ -28,7 +30,9 @@ func newMockDriver() *mockDriver { lbs: make(map[string]*driver.LBInfo), targetGroups: make(map[string]*driver.TargetGroupInfo), listeners: make(map[string]*driver.ListenerInfo), + rules: make(map[string]*driver.RuleInfo), targets: make(map[string][]driver.TargetHealth), + attrs: make(map[string]driver.LBAttributes), } } @@ -201,6 +205,83 @@ func (m *mockDriver) SetTargetHealth(_ context.Context, tgARN, targetID, state s return fmt.Errorf("target not found") } +func (m *mockDriver) CreateRule(_ context.Context, config driver.RuleConfig) (*driver.RuleInfo, error) { + if _, ok := m.listeners[config.ListenerARN]; !ok { + return nil, fmt.Errorf("listener not found") + } + + arn := "arn:rule/" + m.nextID("rule") + info := &driver.RuleInfo{ + ARN: arn, ListenerARN: config.ListenerARN, Priority: config.Priority, + Conditions: config.Conditions, Actions: config.Actions, + } + m.rules[arn] = info + + return info, nil +} + +func (m *mockDriver) DeleteRule(_ context.Context, ruleARN string) error { + if _, ok := m.rules[ruleARN]; !ok { + return fmt.Errorf("rule not found") + } + + delete(m.rules, ruleARN) + + return nil +} + +func (m *mockDriver) DescribeRules(_ context.Context, listenerARN string) ([]driver.RuleInfo, error) { + var result []driver.RuleInfo + + for _, r := range m.rules { + if r.ListenerARN == listenerARN { + result = append(result, *r) + } + } + + return result, nil +} + +func (m *mockDriver) ModifyListener(_ context.Context, input driver.ModifyListenerInput) error { + li, ok := m.listeners[input.ListenerARN] + if !ok { + return fmt.Errorf("listener not found") + } + + if input.Port != 0 { + li.Port = input.Port + } + + if input.Protocol != "" { + li.Protocol = input.Protocol + } + + return nil +} + +func (m *mockDriver) GetLBAttributes(_ context.Context, lbARN string) (*driver.LBAttributes, error) { + if _, ok := m.lbs[lbARN]; !ok { + return nil, fmt.Errorf("lb not found") + } + + attrs, ok := m.attrs[lbARN] + if !ok { + attrs = driver.LBAttributes{IdleTimeout: 60} + } + + return &attrs, nil +} + +func (m *mockDriver) PutLBAttributes(_ context.Context, lbARN string, attrs driver.LBAttributes) error { + if _, ok := m.lbs[lbARN]; !ok { + return fmt.Errorf("lb not found") + } + + m.attrs[lbARN] = attrs + + return nil +} + func newTestLB(opts ...Option) *LB { return NewLB(newMockDriver(), opts...) } diff --git a/providers/aws/elb/elb.go b/providers/aws/elb/elb.go index 82b383b6..9be13157 100644 --- a/providers/aws/elb/elb.go +++ b/providers/aws/elb/elb.go @@ -16,15 +16,22 @@ import ( // Compile-time check that Mock implements driver.LoadBalancer. var _ driver.LoadBalancer = (*Mock)(nil) +// defaultIdleTimeoutSec is the default idle timeout for load balancers in seconds. +const defaultIdleTimeoutSec = 60 + // Mock is an in-memory mock implementation of the AWS ELB service. type Mock struct { lbs *memstore.Store[driver.LBInfo] tgs *memstore.Store[driver.TargetGroupInfo] listeners *memstore.Store[driver.ListenerInfo] + rules *memstore.Store[driver.RuleInfo] opts *config.Options healthMu sync.RWMutex health map[string]map[string]*driver.TargetHealth // tgARN -> targetID -> health + + attrsMu sync.RWMutex + attrs map[string]driver.LBAttributes // lbARN -> attributes } // New creates a new ELB mock with the given configuration options. @@ -33,8 +40,10 @@ func New(opts *config.Options) *Mock { lbs: memstore.New[driver.LBInfo](), tgs: memstore.New[driver.TargetGroupInfo](), listeners: memstore.New[driver.ListenerInfo](), + rules: memstore.New[driver.RuleInfo](), opts: opts, health: make(map[string]map[string]*driver.TargetHealth), + attrs: make(map[string]driver.LBAttributes), } } @@ -186,6 +195,18 @@ func describeResources[T any](store *memstore.Store[T], keys []string) []T { return results } +// filterToSlice returns a slice of values from the store that match the predicate. +func filterToSlice[T any](store *memstore.Store[T], pred func(string, T) bool) []T { + filtered := store.Filter(pred) + + results := make([]T, 0, len(filtered)) + for _, item := range filtered { + results = append(results, item) + } + + return results +} + // CreateListener creates a new listener on a load balancer. func (m *Mock) CreateListener(_ context.Context, cfg driver.ListenerConfig) (*driver.ListenerInfo, error) { if _, ok := m.lbs.Get(cfg.LBARN); !ok { @@ -225,16 +246,114 @@ func (m *Mock) DescribeListeners(_ context.Context, lbARN string) ([]driver.List return nil, errors.Newf(errors.NotFound, "load balancer %q not found", lbARN) } - filtered := m.listeners.Filter(func(_ string, li driver.ListenerInfo) bool { + return filterToSlice(m.listeners, func(_ string, li driver.ListenerInfo) bool { return li.LBARN == lbARN - }) + }), nil +} - results := make([]driver.ListenerInfo, 0, len(filtered)) - for _, li := range filtered { - results = append(results, li) +// CreateRule creates a new listener rule. +func (m *Mock) CreateRule(_ context.Context, cfg driver.RuleConfig) (*driver.RuleInfo, error) { + if _, ok := m.listeners.Get(cfg.ListenerARN); !ok { + return nil, errors.Newf(errors.NotFound, "listener %q not found", cfg.ListenerARN) } - return results, nil + arn := idgen.AWSARN("elasticloadbalancing", m.opts.Region, m.opts.AccountID, + fmt.Sprintf("rule/%s/%s", cfg.ListenerARN, idgen.GenerateID("rule-"))) + + conditions := make([]driver.RuleCondition, len(cfg.Conditions)) + copy(conditions, cfg.Conditions) + + actions := make([]driver.RuleAction, len(cfg.Actions)) + copy(actions, cfg.Actions) + + rule := driver.RuleInfo{ + ARN: arn, + ListenerARN: cfg.ListenerARN, + Priority: cfg.Priority, + Conditions: conditions, + Actions: actions, + IsDefault: false, + } + + m.rules.Set(arn, rule) + + result := rule + + return &result, nil +} + +// DeleteRule deletes a listener rule by ARN. +func (m *Mock) DeleteRule(_ context.Context, ruleARN string) error { + if !m.rules.Delete(ruleARN) { + return errors.Newf(errors.NotFound, "rule %q not found", ruleARN) + } + + return nil +} + +// DescribeRules returns all rules for the specified listener. +func (m *Mock) DescribeRules(_ context.Context, listenerARN string) ([]driver.RuleInfo, error) { + if _, ok := m.listeners.Get(listenerARN); !ok { + return nil, errors.Newf(errors.NotFound, "listener %q not found", listenerARN) + } + + return filterToSlice(m.rules, func(_ string, r driver.RuleInfo) bool { + return r.ListenerARN == listenerARN + }), nil +} + +// ModifyListener modifies an existing listener's port, protocol, or default actions. +func (m *Mock) ModifyListener(_ context.Context, input driver.ModifyListenerInput) error { + li, ok := m.listeners.Get(input.ListenerARN) + if !ok { + return errors.Newf(errors.NotFound, "listener %q not found", input.ListenerARN) + } + + if input.Port != 0 { + li.Port = input.Port + } + + if input.Protocol != "" { + li.Protocol = input.Protocol + } + + if len(input.DefaultActions) > 0 { + li.TargetGroupARN = input.DefaultActions[0].TargetGroupARN + } + + m.listeners.Set(input.ListenerARN, li) + + return nil +} + +// GetLBAttributes returns the attributes for a load balancer. +func (m *Mock) GetLBAttributes(_ context.Context, lbARN string) (*driver.LBAttributes, error) { + if _, ok := m.lbs.Get(lbARN); !ok { + return nil, errors.Newf(errors.NotFound, "load balancer %q not found", lbARN) + } + + m.attrsMu.RLock() + defer m.attrsMu.RUnlock() + + attrs, ok := m.attrs[lbARN] + if !ok { + attrs = driver.LBAttributes{IdleTimeout: defaultIdleTimeoutSec} + } + + return &attrs, nil +} + +// PutLBAttributes sets the attributes for a load balancer. +func (m *Mock) PutLBAttributes(_ context.Context, lbARN string, attrs driver.LBAttributes) error { + if _, ok := m.lbs.Get(lbARN); !ok { + return errors.Newf(errors.NotFound, "load balancer %q not found", lbARN) + } + + m.attrsMu.Lock() + m.attrs[lbARN] = attrs + m.attrsMu.Unlock() + + return nil } // RegisterTargets registers targets with a target group. diff --git a/providers/aws/elb/elb_test.go b/providers/aws/elb/elb_test.go index bc836919..e04e7a02 100644 --- a/providers/aws/elb/elb_test.go +++ b/providers/aws/elb/elb_test.go @@ -323,6 +323,157 @@ func TestSetTargetHealth(t *testing.T) { }) } +func TestCreateRule(t *testing.T) { + m := newTestMock() + ctx := context.Background() + lb := createTestLB(m) + tg := createTestTG(m) + li, _ := m.CreateListener(ctx, driver.ListenerConfig{ + LBARN: lb.ARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tg.ARN, + }) + + t.Run("success", func(t *testing.T) { + rule, err := m.CreateRule(ctx, driver.RuleConfig{ + ListenerARN: li.ARN, + Priority: 10, + Conditions: []driver.RuleCondition{{Field: "path-pattern", Values: []string{"/api/*"}}}, + Actions: []driver.RuleAction{{Type: "forward", TargetGroupARN: tg.ARN}}, + }) + requireNoError(t, err) + assertNotEmpty(t, rule.ARN) + assertEqual(t, li.ARN, rule.ListenerARN) + assertEqual(t, 10, rule.Priority) + assertEqual(t, false, rule.IsDefault) + }) + + t.Run("listener not found", func(t *testing.T) { + _, err := m.CreateRule(ctx, driver.RuleConfig{ListenerARN: "arn:nope"}) + assertError(t, err, true) + }) +} + +func TestDeleteRule(t *testing.T) { + m := newTestMock() + ctx := context.Background() + lb := createTestLB(m) + li, _ := m.CreateListener(ctx, driver.ListenerConfig{ + LBARN: lb.ARN, Protocol: "HTTP", Port: 80, + }) + rule, _ := m.CreateRule(ctx, driver.RuleConfig{ + ListenerARN: li.ARN, Priority: 10, + }) + + requireNoError(t, m.DeleteRule(ctx, rule.ARN)) + assertError(t, m.DeleteRule(ctx, "arn:nope"), true) +} + +func TestDescribeRules(t *testing.T) { + m := newTestMock() + ctx := context.Background() + lb := createTestLB(m) + tg := createTestTG(m) + li, _ := m.CreateListener(ctx, driver.ListenerConfig{ + LBARN: lb.ARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tg.ARN, + }) + + _, _ = m.CreateRule(ctx, driver.RuleConfig{ + ListenerARN: li.ARN, Priority: 10, + Conditions: []driver.RuleCondition{{Field: "path-pattern", Values: []string{"/api/*"}}}, + Actions: []driver.RuleAction{{Type: "forward", TargetGroupARN: tg.ARN}}, + }) + _, _ = m.CreateRule(ctx, driver.RuleConfig{ + ListenerARN: li.ARN, Priority: 20, + Conditions: []driver.RuleCondition{{Field: "host-header", Values: []string{"example.com"}}}, + Actions: []driver.RuleAction{{Type: "forward", TargetGroupARN: tg.ARN}}, + }) + + t.Run("success", func(t *testing.T) { + rules, err := m.DescribeRules(ctx, li.ARN) + requireNoError(t, err) + assertEqual(t, 2, len(rules)) + }) + + t.Run("listener not found", func(t *testing.T) { + _, err := m.DescribeRules(ctx, "arn:nope") + assertError(t, err, true) + }) +} + +func TestModifyListener(t *testing.T) { + m := newTestMock() + ctx := context.Background() + lb := createTestLB(m) + tg := createTestTG(m) + li, _ := m.CreateListener(ctx, driver.ListenerConfig{ + LBARN: lb.ARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tg.ARN, + }) + + t.Run("modify port", func(t *testing.T) { + err := m.ModifyListener(ctx, driver.ModifyListenerInput{ + ListenerARN: li.ARN, Port: 8080, + }) + requireNoError(t, err) + + listeners, _ := m.DescribeListeners(ctx, lb.ARN) + assertEqual(t, 8080, listeners[0].Port) + }) + + t.Run("modify protocol", func(t *testing.T) { + err := m.ModifyListener(ctx, driver.ModifyListenerInput{ + ListenerARN: li.ARN, Protocol: "HTTPS", + }) + requireNoError(t, err) + + listeners, _ := m.DescribeListeners(ctx, lb.ARN) + assertEqual(t, "HTTPS", listeners[0].Protocol) + }) + + t.Run("listener not found", func(t *testing.T) { + err := m.ModifyListener(ctx, driver.ModifyListenerInput{ListenerARN: "arn:nope", Port: 80}) + assertError(t, err, true) + }) +} + +func TestLBAttributes(t *testing.T) { + m := newTestMock() + ctx := context.Background() + lb := createTestLB(m) + + t.Run("default attributes", func(t *testing.T) { + attrs, err := m.GetLBAttributes(ctx, lb.ARN) + requireNoError(t, err) + assertEqual(t, 60, attrs.IdleTimeout) + assertEqual(t, false, attrs.DeletionProtection) + }) + + t.Run("put and get", func(t *testing.T) { + err := m.PutLBAttributes(ctx, lb.ARN, driver.LBAttributes{ + IdleTimeout: 120, + DeletionProtection: true, + AccessLogsEnabled: true, + AccessLogsBucket: "my-logs", + }) + requireNoError(t, err) + + attrs, err := m.GetLBAttributes(ctx, lb.ARN) + requireNoError(t, err) + assertEqual(t, 120, attrs.IdleTimeout) + assertEqual(t, true, attrs.DeletionProtection) + assertEqual(t, true, attrs.AccessLogsEnabled) + assertEqual(t, "my-logs", attrs.AccessLogsBucket) + }) + + t.Run("LB not found get", func(t *testing.T) { + _, err := m.GetLBAttributes(ctx, "arn:nope") + assertError(t, err, true) + }) + + t.Run("LB not found put", func(t *testing.T) { + err := m.PutLBAttributes(ctx, "arn:nope", driver.LBAttributes{}) + assertError(t, err, true) + }) +} + // --- test helpers --- func requireNoError(t *testing.T, err error) { diff --git a/providers/azure/azurelb/lb.go b/providers/azure/azurelb/lb.go index 334d4d70..6542ce9b 100644 --- a/providers/azure/azurelb/lb.go +++ b/providers/azure/azurelb/lb.go @@ -16,15 +16,22 @@ import ( // Compile-time check that Mock implements driver.LoadBalancer. var _ driver.LoadBalancer = (*Mock)(nil) +// defaultIdleTimeoutSec is the default idle timeout for load balancers in seconds. +const defaultIdleTimeoutSec = 60 + // Mock is an in-memory mock implementation of the Azure Load Balancer service. type Mock struct { lbs *memstore.Store[driver.LBInfo] tgs *memstore.Store[driver.TargetGroupInfo] listeners *memstore.Store[driver.ListenerInfo] + rules *memstore.Store[driver.RuleInfo] opts *config.Options healthMu sync.RWMutex health map[string]map[string]*driver.TargetHealth // tgARN -> targetID -> health + + attrsMu sync.RWMutex + attrs map[string]driver.LBAttributes // lbARN -> attributes } // New creates a new Azure Load Balancer mock with the given configuration options. @@ -33,8 +40,10 @@ func New(opts *config.Options) *Mock { lbs: memstore.New[driver.LBInfo](), tgs: memstore.New[driver.TargetGroupInfo](), listeners: memstore.New[driver.ListenerInfo](), + rules: memstore.New[driver.RuleInfo](), opts: opts, health: make(map[string]map[string]*driver.TargetHealth), + attrs: make(map[string]driver.LBAttributes), } } @@ -121,6 +130,18 @@ func describeResources[T any](store *memstore.Store[T], keys []string) []T { return results } +// filterToSlice returns a slice of values from the store that match the predicate. +func filterToSlice[T any](store *memstore.Store[T], pred func(string, T) bool) []T { + filtered := store.Filter(pred) + + results := make([]T, 0, len(filtered)) + for _, item := range filtered { + results = append(results, item) + } + + return results +} + // DescribeLoadBalancers returns load balancers matching the given ARNs. // If arns is empty, all load balancers are returned. func (m *Mock) DescribeLoadBalancers(_ context.Context, arns []string) ([]driver.LBInfo, error) { @@ -225,16 +246,114 @@ func (m *Mock) DescribeListeners(_ context.Context, lbARN string) ([]driver.List return nil, cerrors.Newf(cerrors.NotFound, "load balancer %q not found", lbARN) } - filtered := m.listeners.Filter(func(_ string, li driver.ListenerInfo) bool { + return filterToSlice(m.listeners, func(_ string, li driver.ListenerInfo) bool { return li.LBARN == lbARN - }) + }), nil +} - results := make([]driver.ListenerInfo, 0, len(filtered)) - for _, li := range filtered { - results = append(results, li) +// CreateRule creates a new routing rule for a load balancing rule (listener). +func (m *Mock) CreateRule(_ context.Context, cfg driver.RuleConfig) (*driver.RuleInfo, error) { + if _, ok := m.listeners.Get(cfg.ListenerARN); !ok { + return nil, cerrors.Newf(cerrors.NotFound, "listener %q not found", cfg.ListenerARN) } - return results, nil + arn := idgen.AzureID(m.opts.AccountID, "cloud-mock", "Microsoft.Network", + "routingRules", idgen.GenerateID("rule-")) + + conditions := make([]driver.RuleCondition, len(cfg.Conditions)) + copy(conditions, cfg.Conditions) + + actions := make([]driver.RuleAction, len(cfg.Actions)) + copy(actions, cfg.Actions) + + rule := driver.RuleInfo{ + ARN: arn, + ListenerARN: cfg.ListenerARN, + Priority: cfg.Priority, + Conditions: conditions, + Actions: actions, + IsDefault: false, + } + + m.rules.Set(arn, rule) + + result := rule + + return &result, nil +} + +// DeleteRule deletes a routing rule by ARN. +func (m *Mock) DeleteRule(_ context.Context, ruleARN string) error { + if !m.rules.Delete(ruleARN) { + return cerrors.Newf(cerrors.NotFound, "rule %q not found", ruleARN) + } + + return nil +} + +// DescribeRules returns all routing rules for the specified listener. +func (m *Mock) DescribeRules(_ context.Context, listenerARN string) ([]driver.RuleInfo, error) { + if _, ok := m.listeners.Get(listenerARN); !ok { + return nil, cerrors.Newf(cerrors.NotFound, "listener %q not found", listenerARN) + } + + return filterToSlice(m.rules, func(_ string, r driver.RuleInfo) bool { + return r.ListenerARN == listenerARN + }), nil +} + +// ModifyListener modifies an existing load balancing rule's port, protocol, or default actions. +func (m *Mock) ModifyListener(_ context.Context, input driver.ModifyListenerInput) error { + li, ok := m.listeners.Get(input.ListenerARN) + if !ok { + return cerrors.Newf(cerrors.NotFound, "listener %q not found", input.ListenerARN) + } + + if input.Port != 0 { + li.Port = input.Port + } + + if input.Protocol != "" { + li.Protocol = input.Protocol + } + + if len(input.DefaultActions) > 0 { + li.TargetGroupARN = input.DefaultActions[0].TargetGroupARN + } + + m.listeners.Set(input.ListenerARN, li) + + return nil +} + +// GetLBAttributes returns the attributes for a load balancer. +func (m *Mock) GetLBAttributes(_ context.Context, lbARN string) (*driver.LBAttributes, error) { + if _, ok := m.lbs.Get(lbARN); !ok { + return nil, cerrors.Newf(cerrors.NotFound, "load balancer %q not found", lbARN) + } + + m.attrsMu.RLock() + defer m.attrsMu.RUnlock() + + attrs, ok := m.attrs[lbARN] + if !ok { + attrs = driver.LBAttributes{IdleTimeout: defaultIdleTimeoutSec} + } + + return &attrs, nil +} + +// PutLBAttributes sets the attributes for a load balancer. +func (m *Mock) PutLBAttributes(_ context.Context, lbARN string, attrs driver.LBAttributes) error { + if _, ok := m.lbs.Get(lbARN); !ok { + return cerrors.Newf(cerrors.NotFound, "load balancer %q not found", lbARN) + } + + m.attrsMu.Lock() + m.attrs[lbARN] = attrs + m.attrsMu.Unlock() + + return nil } // RegisterTargets registers targets (backend instances) with a backend pool. diff --git a/providers/azure/azurelb/lb_test.go b/providers/azure/azurelb/lb_test.go index acbe87b1..79328dbb 100644 --- a/providers/azure/azurelb/lb_test.go +++ b/providers/azure/azurelb/lb_test.go @@ -409,6 +409,163 @@ func TestDescribeTargetHealthNotFound(t *testing.T) { assert.Contains(t, err.Error(), "not found") } +func TestCreateRule(t *testing.T) { + ctx := context.Background() + m := newTestMock() + lbARN := createTestLB(t, m) + tgARN := createTestTargetGroup(t, m) + + li, err := m.CreateListener(ctx, driver.ListenerConfig{ + LBARN: lbARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tgARN, + }) + require.NoError(t, err) + + t.Run("success", func(t *testing.T) { + rule, ruleErr := m.CreateRule(ctx, driver.RuleConfig{ + ListenerARN: li.ARN, + Priority: 10, + Conditions: []driver.RuleCondition{{Field: "path-pattern", Values: []string{"/api/*"}}}, + Actions: []driver.RuleAction{{Type: "forward", TargetGroupARN: tgARN}}, + }) + require.NoError(t, ruleErr) + assert.NotEmpty(t, rule.ARN) + assert.Equal(t, li.ARN, rule.ListenerARN) + assert.Equal(t, 10, rule.Priority) + assert.False(t, rule.IsDefault) + }) + + t.Run("listener not found", func(t *testing.T) { + _, ruleErr := m.CreateRule(ctx, driver.RuleConfig{ListenerARN: "missing"}) + require.Error(t, ruleErr) + assert.Contains(t, ruleErr.Error(), "not found") + }) +} + +func TestDeleteRule(t *testing.T) { + ctx := context.Background() + m := newTestMock() + lbARN := createTestLB(t, m) + + li, err := m.CreateListener(ctx, driver.ListenerConfig{LBARN: lbARN, Protocol: "HTTP", Port: 80}) + require.NoError(t, err) + + rule, err := m.CreateRule(ctx, driver.RuleConfig{ListenerARN: li.ARN, Priority: 10}) + require.NoError(t, err) + + t.Run("success", func(t *testing.T) { + require.NoError(t, m.DeleteRule(ctx, rule.ARN)) + }) + + t.Run("not found", func(t *testing.T) { + err := m.DeleteRule(ctx, "missing") + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) +} + +func TestDescribeRules(t *testing.T) { + ctx := context.Background() + m := newTestMock() + lbARN := createTestLB(t, m) + tgARN := createTestTargetGroup(t, m) + + li, err := m.CreateListener(ctx, driver.ListenerConfig{ + LBARN: lbARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tgARN, + }) + require.NoError(t, err) + + _, _ = m.CreateRule(ctx, driver.RuleConfig{ + ListenerARN: li.ARN, Priority: 10, + Conditions: []driver.RuleCondition{{Field: "path-pattern", Values: []string{"/api/*"}}}, + Actions: []driver.RuleAction{{Type: "forward", TargetGroupARN: tgARN}}, + }) + _, _ = m.CreateRule(ctx, driver.RuleConfig{ + ListenerARN: li.ARN, Priority: 20, + Conditions: []driver.RuleCondition{{Field: "host-header", Values: []string{"example.com"}}}, + Actions: []driver.RuleAction{{Type: "forward", TargetGroupARN: tgARN}}, + }) + + t.Run("success", func(t *testing.T) { + rules, descErr := m.DescribeRules(ctx, li.ARN) + require.NoError(t, descErr) + assert.Len(t, rules, 2) + }) + + t.Run("listener not found", func(t *testing.T) { + _, descErr := m.DescribeRules(ctx, "missing") + require.Error(t, descErr) + assert.Contains(t, descErr.Error(), "not found") + }) +} + +func TestModifyListener(t *testing.T) { + ctx := context.Background() + m := newTestMock() + lbARN := createTestLB(t, m) + tgARN := createTestTargetGroup(t, m) + + li, err := m.CreateListener(ctx, driver.ListenerConfig{ + LBARN: lbARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tgARN, + }) + require.NoError(t, err) + + t.Run("modify port", func(t *testing.T) { + require.NoError(t, m.ModifyListener(ctx, driver.ModifyListenerInput{ + ListenerARN: li.ARN, Port: 8080, + })) + + listeners, _ := m.DescribeListeners(ctx, lbARN) + assert.Equal(t, 8080, listeners[0].Port) + }) + + t.Run("not found", func(t *testing.T) { + err := m.ModifyListener(ctx, driver.ModifyListenerInput{ListenerARN: "missing", Port: 80}) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) +} + +func TestLBAttributes(t *testing.T) { + ctx := context.Background() + m := newTestMock() + lbARN := createTestLB(t, m) + + t.Run("default attributes", func(t *testing.T) { + attrs, err := m.GetLBAttributes(ctx, lbARN) + require.NoError(t, err) + assert.Equal(t, 60, attrs.IdleTimeout) + assert.False(t, attrs.DeletionProtection) + }) + + t.Run("put and get", func(t *testing.T) { + require.NoError(t, m.PutLBAttributes(ctx, lbARN, driver.LBAttributes{ + IdleTimeout: 120, + DeletionProtection: true, + AccessLogsEnabled: true, + AccessLogsBucket: "my-logs", + })) + + attrs, err := m.GetLBAttributes(ctx, lbARN) + require.NoError(t, err) + assert.Equal(t, 120, attrs.IdleTimeout) + assert.True(t, attrs.DeletionProtection) + assert.True(t, attrs.AccessLogsEnabled) + assert.Equal(t, "my-logs", attrs.AccessLogsBucket) + }) + + t.Run("LB not found get", func(t *testing.T) { + _, err := m.GetLBAttributes(ctx, "missing") + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) + + t.Run("LB not found put", func(t *testing.T) { + err := m.PutLBAttributes(ctx, "missing", driver.LBAttributes{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) +} + func TestDeleteLBCascadesListeners(t *testing.T) { ctx := context.Background() m := newTestMock() diff --git a/providers/gcp/gcplb/lb.go b/providers/gcp/gcplb/lb.go index cfccfcc5..db5f0587 100644 --- a/providers/gcp/gcplb/lb.go +++ b/providers/gcp/gcplb/lb.go @@ -16,15 +16,22 @@ import ( // Compile-time check that Mock implements driver.LoadBalancer. var _ driver.LoadBalancer = (*Mock)(nil) +// defaultIdleTimeoutSec is the default idle timeout for load balancers in seconds. +const defaultIdleTimeoutSec = 60 + // Mock is an in-memory mock implementation of the GCP Cloud Load Balancing service. type Mock struct { lbs *memstore.Store[driver.LBInfo] tgs *memstore.Store[driver.TargetGroupInfo] listeners *memstore.Store[driver.ListenerInfo] + rules *memstore.Store[driver.RuleInfo] opts *config.Options healthMu sync.RWMutex health map[string]map[string]*driver.TargetHealth // tgARN -> targetID -> health + + attrsMu sync.RWMutex + attrs map[string]driver.LBAttributes // lbARN -> attributes } // New creates a new Cloud Load Balancing mock with the given configuration options. @@ -33,8 +40,10 @@ func New(opts *config.Options) *Mock { lbs: memstore.New[driver.LBInfo](), tgs: memstore.New[driver.TargetGroupInfo](), listeners: memstore.New[driver.ListenerInfo](), + rules: memstore.New[driver.RuleInfo](), opts: opts, health: make(map[string]map[string]*driver.TargetHealth), + attrs: make(map[string]driver.LBAttributes), } } @@ -186,6 +195,18 @@ func describeResources[T any](store *memstore.Store[T], keys []string) []T { return results } +// filterToSlice returns a slice of values from the store that match the predicate. +func filterToSlice[T any](store *memstore.Store[T], pred func(string, T) bool) []T { + filtered := store.Filter(pred) + + results := make([]T, 0, len(filtered)) + for _, item := range filtered { + results = append(results, item) + } + + return results +} + // CreateListener creates a new URL map / listener on a load balancer. func (m *Mock) CreateListener(_ context.Context, cfg driver.ListenerConfig) (*driver.ListenerInfo, error) { if _, ok := m.lbs.Get(cfg.LBARN); !ok { @@ -225,16 +246,113 @@ func (m *Mock) DescribeListeners(_ context.Context, lbARN string) ([]driver.List return nil, cerrors.Newf(cerrors.NotFound, "load balancer %q not found", lbARN) } - filtered := m.listeners.Filter(func(_ string, li driver.ListenerInfo) bool { + return filterToSlice(m.listeners, func(_ string, li driver.ListenerInfo) bool { return li.LBARN == lbARN - }) + }), nil +} - results := make([]driver.ListenerInfo, 0, len(filtered)) - for _, li := range filtered { - results = append(results, li) +// CreateRule creates a new URL map path rule for a listener. +func (m *Mock) CreateRule(_ context.Context, cfg driver.RuleConfig) (*driver.RuleInfo, error) { + if _, ok := m.listeners.Get(cfg.ListenerARN); !ok { + return nil, cerrors.Newf(cerrors.NotFound, "listener %q not found", cfg.ListenerARN) } - return results, nil + arn := idgen.GCPID(m.opts.ProjectID, "pathRules", idgen.GenerateID("rule-")) + + conditions := make([]driver.RuleCondition, len(cfg.Conditions)) + copy(conditions, cfg.Conditions) + + actions := make([]driver.RuleAction, len(cfg.Actions)) + copy(actions, cfg.Actions) + + rule := driver.RuleInfo{ + ARN: arn, + ListenerARN: cfg.ListenerARN, + Priority: cfg.Priority, + Conditions: conditions, + Actions: actions, + IsDefault: false, + } + + m.rules.Set(arn, rule) + + result := rule + + return &result, nil +} + +// DeleteRule deletes a URL map path rule by resource name (ARN). +func (m *Mock) DeleteRule(_ context.Context, ruleARN string) error { + if !m.rules.Delete(ruleARN) { + return cerrors.Newf(cerrors.NotFound, "rule %q not found", ruleARN) + } + + return nil +} + +// DescribeRules returns all path rules for the specified listener. +func (m *Mock) DescribeRules(_ context.Context, listenerARN string) ([]driver.RuleInfo, error) { + if _, ok := m.listeners.Get(listenerARN); !ok { + return nil, cerrors.Newf(cerrors.NotFound, "listener %q not found", listenerARN) + } + + return filterToSlice(m.rules, func(_ string, r driver.RuleInfo) bool { + return r.ListenerARN == listenerARN + }), nil +} + +// ModifyListener modifies an existing URL map listener's port, protocol, or default actions. +func (m *Mock) ModifyListener(_ context.Context, input driver.ModifyListenerInput) error { + li, ok := m.listeners.Get(input.ListenerARN) + if !ok { + return cerrors.Newf(cerrors.NotFound, "listener %q not found", input.ListenerARN) + } + + if input.Port != 0 { + li.Port = input.Port + } + + if input.Protocol != "" { + li.Protocol = input.Protocol + } + + if len(input.DefaultActions) > 0 { + li.TargetGroupARN = input.DefaultActions[0].TargetGroupARN + } + + m.listeners.Set(input.ListenerARN, li) + + return nil +} + +// GetLBAttributes returns the attributes for a load balancer. +func (m *Mock) GetLBAttributes(_ context.Context, lbARN string) (*driver.LBAttributes, error) { + if _, ok := m.lbs.Get(lbARN); !ok { + return nil, cerrors.Newf(cerrors.NotFound, "load balancer %q not found", lbARN) + } + + m.attrsMu.RLock() + defer m.attrsMu.RUnlock() + + attrs, ok := m.attrs[lbARN] + if !ok { + attrs = driver.LBAttributes{IdleTimeout: defaultIdleTimeoutSec} + } + + return &attrs, nil +} + +// PutLBAttributes sets the attributes for a load balancer. +func (m *Mock) PutLBAttributes(_ context.Context, lbARN string, attrs driver.LBAttributes) error { + if _, ok := m.lbs.Get(lbARN); !ok { + return cerrors.Newf(cerrors.NotFound, "load balancer %q not found", lbARN) + } + + m.attrsMu.Lock() + m.attrs[lbARN] = attrs + m.attrsMu.Unlock() + + return nil } // RegisterTargets adds instances to a backend service (target group). diff --git a/providers/gcp/gcplb/lb_test.go b/providers/gcp/gcplb/lb_test.go index ed7c257d..62cff86b 100644 --- a/providers/gcp/gcplb/lb_test.go +++ b/providers/gcp/gcplb/lb_test.go @@ -355,6 +355,179 @@ func TestDescribeListeners(t *testing.T) { }) } +func TestCreateRule(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + lb, err := m.CreateLoadBalancer(ctx, driver.LBConfig{Name: "lb1"}) + require.NoError(t, err) + + tg, err := m.CreateTargetGroup(ctx, driver.TargetGroupConfig{Name: "tg1", Protocol: "HTTP", Port: 80}) + require.NoError(t, err) + + li, err := m.CreateListener(ctx, driver.ListenerConfig{ + LBARN: lb.ARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tg.ARN, + }) + require.NoError(t, err) + + t.Run("success", func(t *testing.T) { + rule, ruleErr := m.CreateRule(ctx, driver.RuleConfig{ + ListenerARN: li.ARN, + Priority: 10, + Conditions: []driver.RuleCondition{{Field: "path-pattern", Values: []string{"/api/*"}}}, + Actions: []driver.RuleAction{{Type: "forward", TargetGroupARN: tg.ARN}}, + }) + require.NoError(t, ruleErr) + assert.NotEmpty(t, rule.ARN) + assert.Equal(t, li.ARN, rule.ListenerARN) + assert.Equal(t, 10, rule.Priority) + assert.False(t, rule.IsDefault) + }) + + t.Run("listener not found", func(t *testing.T) { + _, ruleErr := m.CreateRule(ctx, driver.RuleConfig{ListenerARN: "missing"}) + require.Error(t, ruleErr) + assert.Contains(t, ruleErr.Error(), "not found") + }) +} + +func TestDeleteRule(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + lb, err := m.CreateLoadBalancer(ctx, driver.LBConfig{Name: "lb1"}) + require.NoError(t, err) + + li, err := m.CreateListener(ctx, driver.ListenerConfig{LBARN: lb.ARN, Protocol: "HTTP", Port: 80}) + require.NoError(t, err) + + rule, err := m.CreateRule(ctx, driver.RuleConfig{ListenerARN: li.ARN, Priority: 10}) + require.NoError(t, err) + + t.Run("success", func(t *testing.T) { + require.NoError(t, m.DeleteRule(ctx, rule.ARN)) + }) + + t.Run("not found", func(t *testing.T) { + err := m.DeleteRule(ctx, "missing") + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) +} + +func TestDescribeRules(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + lb, err := m.CreateLoadBalancer(ctx, driver.LBConfig{Name: "lb1"}) + require.NoError(t, err) + + tg, err := m.CreateTargetGroup(ctx, driver.TargetGroupConfig{Name: "tg1", Protocol: "HTTP", Port: 80}) + require.NoError(t, err) + + li, err := m.CreateListener(ctx, driver.ListenerConfig{ + LBARN: lb.ARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tg.ARN, + }) + require.NoError(t, err) + + _, _ = m.CreateRule(ctx, driver.RuleConfig{ + ListenerARN: li.ARN, Priority: 10, + Conditions: []driver.RuleCondition{{Field: "path-pattern", Values: []string{"/api/*"}}}, + Actions: []driver.RuleAction{{Type: "forward", TargetGroupARN: tg.ARN}}, + }) + _, _ = m.CreateRule(ctx, driver.RuleConfig{ + ListenerARN: li.ARN, Priority: 20, + Conditions: []driver.RuleCondition{{Field: "host-header", Values: []string{"example.com"}}}, + Actions: []driver.RuleAction{{Type: "forward", TargetGroupARN: tg.ARN}}, + }) + + t.Run("success", func(t *testing.T) { + rules, descErr := m.DescribeRules(ctx, li.ARN) + require.NoError(t, descErr) + assert.Len(t, rules, 2) + }) + + t.Run("listener not found", func(t *testing.T) { + _, descErr := m.DescribeRules(ctx, "missing") + require.Error(t, descErr) + assert.Contains(t, descErr.Error(), "not found") + }) +} + +func TestModifyListener(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + lb, err := m.CreateLoadBalancer(ctx, driver.LBConfig{Name: "lb1"}) + require.NoError(t, err) + + tg, err := m.CreateTargetGroup(ctx, driver.TargetGroupConfig{Name: "tg1", Protocol: "HTTP", Port: 80}) + require.NoError(t, err) + + li, err := m.CreateListener(ctx, driver.ListenerConfig{ + LBARN: lb.ARN, Protocol: "HTTP", Port: 80, TargetGroupARN: tg.ARN, + }) + require.NoError(t, err) + + t.Run("modify port", func(t *testing.T) { + require.NoError(t, m.ModifyListener(ctx, driver.ModifyListenerInput{ + ListenerARN: li.ARN, Port: 8080, + })) + + listeners, _ := m.DescribeListeners(ctx, lb.ARN) + assert.Equal(t, 8080, listeners[0].Port) + }) + + t.Run("not found", func(t *testing.T) { + err := m.ModifyListener(ctx, driver.ModifyListenerInput{ListenerARN: "missing", Port: 80}) + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) +} + +func TestLBAttributes(t *testing.T) { + ctx := context.Background() + m := newTestMock() + + lb, err := m.CreateLoadBalancer(ctx, driver.LBConfig{Name: "lb1"}) + require.NoError(t, err) + + t.Run("default attributes", func(t *testing.T) { + attrs, attrErr := m.GetLBAttributes(ctx, lb.ARN) + require.NoError(t, attrErr) + assert.Equal(t, 60, attrs.IdleTimeout) + assert.False(t, attrs.DeletionProtection) + }) + + t.Run("put and get", func(t *testing.T) { + require.NoError(t, m.PutLBAttributes(ctx, lb.ARN, driver.LBAttributes{ + IdleTimeout: 120, + DeletionProtection: true, + AccessLogsEnabled: true, + AccessLogsBucket: "my-logs", + })) + + attrs, attrErr := m.GetLBAttributes(ctx, lb.ARN) + require.NoError(t, attrErr) + assert.Equal(t, 120, attrs.IdleTimeout) + assert.True(t, attrs.DeletionProtection) + assert.True(t, attrs.AccessLogsEnabled) + assert.Equal(t, "my-logs", attrs.AccessLogsBucket) + }) + + t.Run("LB not found get", func(t *testing.T) { + _, attrErr := m.GetLBAttributes(ctx, "missing") + require.Error(t, attrErr) + assert.Contains(t, attrErr.Error(), "not found") + }) + + t.Run("LB not found put", func(t *testing.T) { + attrErr := m.PutLBAttributes(ctx, "missing", driver.LBAttributes{}) + require.Error(t, attrErr) + assert.Contains(t, attrErr.Error(), "not found") + }) +} + func TestDeleteListenerCleansUpOnLBDelete(t *testing.T) { ctx := context.Background() m := newTestMock() From 3a5b6b822dac25bec6c816d08352075cc902dfd0 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Wed, 1 Apr 2026 02:42:10 +0530 Subject: [PATCH 8/8] add community profile: code of conduct, contributing, security, templates --- .github/ISSUE_TEMPLATE/bug_report.md | 43 +++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 37 +++++++++ .github/pull_request_template.md | 29 +++++++ CODE_OF_CONDUCT.md | 66 ++++++++++++++++ CONTRIBUTING.md | 93 +++++++++++++++++++++++ SECURITY.md | 35 +++++++++ 6 files changed, 303 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/pull_request_template.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..aa8b7f63 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,43 @@ +--- +name: Bug Report +about: Report a bug in CloudEmu +title: "[Bug] " +labels: bug +assignees: '' +--- + +## Describe the Bug + +A clear description of what the bug is. + +## To Reproduce + +Steps to reproduce the behavior: + +1. Create provider with `cloudemu.NewAWS()` +2. Call `...` +3. See error + +## Expected Behavior + +What you expected to happen. + +## Actual Behavior + +What actually happened. Include error messages if applicable. + +## Code Sample + +```go +// Minimal code to reproduce the issue +``` + +## Environment + +- Go version: [e.g., 1.25.0] +- CloudEmu version: [e.g., v0.1.0 or commit hash] +- OS: [e.g., macOS, Linux, Windows] + +## Additional Context + +Any other context about the problem. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..442f930a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,37 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement for CloudEmu +title: "[Feature] " +labels: enhancement +assignees: '' +--- + +## Feature Description + +A clear description of the feature you'd like to see. + +## Provider(s) + +Which cloud provider(s) does this apply to? + +- [ ] AWS +- [ ] Azure +- [ ] GCP + +## Service + +Which service does this relate to? (e.g., Storage, Compute, Database, etc.) + +## Use Case + +Describe the testing scenario this feature would enable. + +## Proposed API + +```go +// Example of what the API could look like +``` + +## Additional Context + +Any other context, links to cloud documentation, or examples. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..f13195d0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ +## Summary + + + +## Changes + + + +## Provider Coverage + +- [ ] AWS +- [ ] Azure +- [ ] GCP + +## Checklist + +- [ ] All tests pass (`go test ./...`) +- [ ] Linter passes (`golangci-lint run --timeout=9m ./...`) +- [ ] All 3 providers implement the same behavior +- [ ] Integration tests added to `cloudemu_test.go` +- [ ] Unit tests added to provider test files + +## Test Plan + + + +## Related Issues + + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..3f703561 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,66 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +**nitinraj7488204975@gmail.com**. + +All complaints will be reviewed and investigated promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..1d5d510f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,93 @@ +# Contributing to CloudEmu + +Thank you for your interest in contributing to CloudEmu! This guide will help you get started. + +## Getting Started + +1. Fork the repository +2. Clone your fork: + ```bash + git clone https://github.com//cloudemu.git + cd cloudemu + ``` +3. Create a feature branch from `development`: + ```bash + git checkout development + git checkout -b feature/your-feature-name + ``` + +## Development Setup + +**Requirements:** +- Go 1.25.0+ +- golangci-lint v2 + +```bash +go build ./... # compile all packages +go test ./... # run all tests +go vet ./... # static analysis +``` + +## Code Standards + +- **Max line length:** 140 characters +- **Max cyclomatic complexity:** 10 +- **Max function length:** 100 lines / 50 statements +- **No magic numbers** — use named constants +- **Import ordering:** stdlib, third-party, local module (enforced by `gci`) +- **Thread safety:** all mock implementations must use `sync.RWMutex` + +### Linting + +Run the linter before submitting: + +```bash +golangci-lint run --timeout=9m ./... +``` + +Fix all issues. If a `//nolint` directive is needed, always include an explanation. + +## Making Changes + +### Adding a New Feature to an Existing Service + +1. Add types and methods to the driver interface (`/driver/driver.go`) +2. Implement in **all 3 providers** (AWS, Azure, GCP) +3. Wire through the portable API layer (`/.go`) +4. Add integration tests to `cloudemu_test.go` +5. Add unit tests to each provider test file +6. Run linter and full test suite + +### Adding a New Service + +1. Create driver interface in `/driver/driver.go` +2. Create provider implementations in `providers/{aws,azure,gcp}//` +3. Add field to each Provider struct +4. Initialize in each `New()` factory +5. Add portable API wrapper +6. Add tests + +### Important Rules + +- All 3 providers (AWS, Azure, GCP) must implement the same behaviors +- Use `cerrors.New()` / `cerrors.Newf()` for error codes +- Use `config.FakeClock` for deterministic time in tests +- Use `memstore.Store[V]` for in-memory storage +- Use `idgen` for cloud-native ID generation + +## Submitting Changes + +1. Ensure all tests pass: `go test ./...` +2. Ensure linter passes: `golangci-lint run --timeout=9m ./...` +3. Push your branch and create a PR against `development` +4. Include a summary of what changed and why in the PR description + +## Reporting Issues + +- Use GitHub Issues to report bugs or request features +- Include steps to reproduce for bug reports +- Tag issues with appropriate labels (aws, azure, gcp, enhancement, bug) + +## License + +By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..1cf7a261 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,35 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +|---------|--------------------| +| latest | :white_check_mark: | + +## Reporting a Vulnerability + +If you discover a security vulnerability in CloudEmu, please report it responsibly. + +**Do NOT open a public GitHub issue for security vulnerabilities.** + +Instead, please email **nitinraj7488204975@gmail.com** with: + +- A description of the vulnerability +- Steps to reproduce the issue +- Any potential impact + +We will acknowledge receipt within 48 hours and aim to provide a fix within 7 days for critical issues. + +## Scope + +CloudEmu is an in-memory testing library and does not handle production traffic, secrets, or real cloud credentials. However, we still take security seriously in the following areas: + +- **Code injection** via user-provided inputs (policy documents, filter patterns) +- **Denial of service** via unbounded memory allocation +- **Dependency vulnerabilities** in Go modules + +## Best Practices for Users + +- Never use CloudEmu in production environments — it is designed for testing only +- Do not commit real cloud credentials in test files +- Keep your Go dependencies up to date with `go get -u ./...`