From 08d646bf059e9d4e7a47179a22970f7ae2e10dac Mon Sep 17 00:00:00 2001 From: Hyko Date: Sun, 12 Apr 2026 12:37:32 -0400 Subject: [PATCH] chore: import project from private repo --- .env.example | 47 + .gitignore | 22 + .npmrc | 1 + README.md | 20 +- docs/INSTALL.md | 61 + docs/dev/GUIDE.md | 38 + docs/dev/REDACTION.md | 163 + package-lock.json | 7598 +++++++++++++++++ package.json | 172 + postcss.config.js | 6 + src/core/api/dynamic-router.js | 303 + src/core/api/handlers/health.js | 13 + src/core/api/handlers/storage.js | 127 + src/core/api/handlers/users.js | 568 ++ src/core/api/handlers/version.js | 16 + src/core/api/index.js | 21 + src/core/api/nx-route.js | 166 + src/core/api/router.js | 320 + src/core/cron/index.js | 183 + src/core/database/cli.js | 127 + src/core/database/crud.js | 223 + src/core/database/db.js | 148 + src/core/database/index.js | 38 + src/core/database/init.js | 187 + src/core/email/index.js | 210 + src/core/email/templates/BaseLayout.jsx | 86 + .../email/templates/PasswordChangedEmail.jsx | 41 + .../email/templates/PasswordResetEmail.jsx | 49 + .../email/templates/VerificationEmail.jsx | 49 + src/core/email/templates/index.js | 71 + src/core/modules/client.js | 32 + src/core/modules/discovery.js | 192 + src/core/modules/index.js | 42 + src/core/modules/loader.js | 244 + src/core/modules/registry.js | 289 + src/core/payments/index.js | 7 + src/core/payments/stripe.js | 270 + src/core/pdf/index.js | 121 + src/core/storage/index.js | 671 ++ src/core/storage/utils.js | 264 + src/core/toast/Toast.js | 133 + src/core/toast/ToastContainer.js | 132 + src/core/toast/ToastContext.js | 110 + src/core/toast/index.js | 6 + src/features/admin/actions.js | 12 + src/features/admin/actions/statsActions.js | 79 + src/features/admin/components/AdminHeader.js | 214 + src/features/admin/components/AdminPages.js | 85 + .../admin/components/AdminPagesLayout.js | 29 + src/features/admin/components/AdminSidebar.js | 234 + src/features/admin/components/ThemeToggle.js | 83 + src/features/admin/components/index.js | 6 + .../admin/components/pages/DashboardPage.js | 64 + .../admin/components/pages/ProfilePage.js | 331 + .../admin/components/pages/UserEditPage.js | 254 + .../admin/components/pages/UsersPage.js | 220 + src/features/admin/index.js | 16 + src/features/admin/middleware/protect.js | 65 + src/features/admin/navigation.server.js | 69 + src/features/admin/page.js | 135 + src/features/admin/pages.js | 11 + src/features/auth/README-custom-login.md | 347 + src/features/auth/README-dashboard.md | 274 + src/features/auth/actions.js | 19 + src/features/auth/actions/authActions.js | 341 + .../auth/components/AccountSection.js | 279 + src/features/auth/components/AuthPages.js | 104 + .../auth/components/AuthPagesLayout.js | 19 + src/features/auth/components/UserAvatar.js | 55 + src/features/auth/components/UserMenu.js | 90 + src/features/auth/components/index.js | 43 + .../auth/components/pages/ConfirmEmailPage.js | 162 + .../components/pages/ForgotPasswordPage.js | 174 + .../auth/components/pages/LoginPage.js | 228 + .../auth/components/pages/LogoutPage.js | 117 + .../auth/components/pages/RegisterPage.js | 337 + .../components/pages/ResetPasswordPage.js | 222 + .../auth/components/useCurrentUser.js | 66 + src/features/auth/index.js | 66 + src/features/auth/lib/auth.js | 295 + src/features/auth/lib/email.js | 233 + src/features/auth/lib/password.js | 65 + src/features/auth/lib/rateLimit.js | 116 + src/features/auth/lib/session.js | 138 + src/features/auth/middleware/protect.js | 83 + src/features/auth/page.js | 46 + src/features/auth/pages.js | 12 + src/features/provider/ZenProvider.js | 12 + src/features/provider/index.js | 3 + src/features/setup/cli.js | 251 + src/features/setup/index.js | 6 + src/index.js | 48 + src/modules/PublicPagesClient.js | 54 + src/modules/PublicPagesLayout.js | 17 + src/modules/README.md | 284 + src/modules/clients/.env.example | 4 + src/modules/clients/INSTALL.md | 37 + src/modules/clients/admin/ClientCreatePage.js | 76 + src/modules/clients/admin/ClientEditPage.js | 139 + src/modules/clients/admin/ClientForm.js | 245 + src/modules/clients/admin/ClientsListPage.js | 277 + src/modules/clients/admin/index.js | 8 + src/modules/clients/api.js | 148 + src/modules/clients/crud.js | 269 + src/modules/clients/db.js | 114 + src/modules/clients/index.js | 28 + src/modules/clients/module.config.js | 50 + src/modules/index.js | 49 + src/modules/init.js | 79 + src/modules/invoice/.env.example | 19 + src/modules/invoice/GUIDE-client-dashboard.md | 142 + src/modules/invoice/README.md | 262 + src/modules/invoice/actions.js | 338 + .../invoice/admin/InvoiceCreatePage.js | 613 ++ src/modules/invoice/admin/InvoiceEditPage.js | 788 ++ src/modules/invoice/admin/InvoicesListPage.js | 354 + src/modules/invoice/admin/index.js | 7 + src/modules/invoice/api.js | 882 ++ .../categories/admin/CategoriesListPage.js | 266 + .../categories/admin/CategoryCreatePage.js | 231 + .../categories/admin/CategoryEditPage.js | 288 + src/modules/invoice/categories/admin/index.js | 9 + src/modules/invoice/categories/crud.js | 317 + src/modules/invoice/categories/db.js | 78 + src/modules/invoice/cron.config.js | 44 + src/modules/invoice/crud.js | 721 ++ .../dashboard/ClientInvoicesSection.js | 258 + .../invoice/dashboard/InvoicesWidget.js | 46 + .../invoice/dashboard/RevenueWidget.js | 77 + src/modules/invoice/dashboard/index.js | 14 + src/modules/invoice/dashboard/statsActions.js | 142 + src/modules/invoice/db.js | 235 + .../email/AdminOverdueNotificationEmail.jsx | 121 + .../invoice/email/InvoiceOverdueEmail.jsx | 110 + .../email/InvoicePaymentConfirmationEmail.jsx | 116 + .../invoice/email/InvoiceReceiptEmail.jsx | 100 + .../invoice/email/InvoiceReminderEmail.jsx | 106 + src/modules/invoice/email/index.js | 9 + src/modules/invoice/index.js | 173 + src/modules/invoice/interest.js | 278 + .../invoice/items/admin/ItemCreatePage.js | 255 + .../invoice/items/admin/ItemEditPage.js | 312 + .../invoice/items/admin/ItemsListPage.js | 267 + src/modules/invoice/items/admin/index.js | 9 + src/modules/invoice/items/crud.js | 248 + src/modules/invoice/items/db.js | 85 + src/modules/invoice/metadata.js | 233 + src/modules/invoice/module.config.js | 112 + .../invoice/pages/InvoicePDFViewerPage.js | 106 + .../invoice/pages/InvoicePublicPages.js | 273 + src/modules/invoice/pages/PaymentPage.js | 410 + .../invoice/pages/ReceiptPDFViewerPage.js | 106 + src/modules/invoice/pages/ThemeToggle.js | 46 + src/modules/invoice/pages/index.js | 10 + .../invoice/pdf/InvoicePDFTemplate.jsx | 334 + .../invoice/pdf/ReceiptPDFTemplate.jsx | 392 + src/modules/invoice/pdf/generatePDF.js | 102 + .../recurrences/admin/RecurrenceCreatePage.js | 691 ++ .../recurrences/admin/RecurrenceEditPage.js | 765 ++ .../recurrences/admin/RecurrencesListPage.js | 360 + .../invoice/recurrences/admin/index.js | 9 + src/modules/invoice/recurrences/crud.js | 369 + src/modules/invoice/recurrences/db.js | 98 + src/modules/invoice/recurrences/processor.js | 119 + src/modules/invoice/reminders.js | 510 ++ .../admin/TransactionCreatePage.js | 371 + .../admin/TransactionsListPage.js | 228 + .../invoice/transactions/admin/index.js | 8 + src/modules/invoice/transactions/crud.js | 377 + src/modules/invoice/transactions/db.js | 82 + src/modules/modules.actions.js | 116 + src/modules/modules.metadata.js | 59 + src/modules/modules.pages.js | 117 + src/modules/modules.registry.js | 25 + src/modules/nuage/.env.example | 4 + src/modules/nuage/GUIDE-client-dashboard.md | 192 + src/modules/nuage/README.md | 154 + src/modules/nuage/actions.js | 274 + src/modules/nuage/admin/ExplorerPage.js | 796 ++ src/modules/nuage/admin/SharesPage.js | 254 + src/modules/nuage/admin/index.js | 5 + src/modules/nuage/api.js | 679 ++ .../nuage/components/FileViewerModal.js | 85 + .../nuage/components/NuageFileTable.js | 219 + src/modules/nuage/components/SharePanel.js | 776 ++ src/modules/nuage/crud.js | 461 + .../nuage/dashboard/ClientNuageSection.js | 424 + src/modules/nuage/dashboard/index.js | 10 + src/modules/nuage/db.js | 109 + src/modules/nuage/email/NuageShareEmail.jsx | 105 + src/modules/nuage/email/index.js | 4 + src/modules/nuage/metadata.js | 69 + src/modules/nuage/module.config.js | 44 + src/modules/nuage/pages/NuagePublicPages.js | 427 + src/modules/nuage/pages/index.js | 4 + src/modules/page.js | 114 + src/modules/pages.js | 19 + src/modules/posts/.env.example | 16 + src/modules/posts/README.md | 547 ++ src/modules/posts/admin/PostCreatePage.js | 245 + src/modules/posts/admin/PostEditPage.js | 271 + src/modules/posts/admin/PostFormFields.js | 359 + src/modules/posts/admin/PostsIndexPage.js | 104 + src/modules/posts/admin/PostsListPage.js | 316 + src/modules/posts/api.js | 472 + .../categories/admin/CategoriesListPage.js | 198 + .../categories/admin/CategoryCreatePage.js | 124 + .../categories/admin/CategoryEditPage.js | 158 + src/modules/posts/categories/crud.js | 183 + src/modules/posts/config.js | 134 + src/modules/posts/crud.js | 525 ++ src/modules/posts/db.js | 140 + src/modules/posts/module.config.js | 98 + src/shared/Icons.js | 581 ++ src/shared/components/Badge.js | 74 + src/shared/components/Breadcrumb.js | 42 + src/shared/components/Button.js | 76 + src/shared/components/Card.js | 110 + src/shared/components/FilterTabs.js | 34 + src/shared/components/Input.js | 115 + src/shared/components/Loading.js | 24 + src/shared/components/LoadingState.js | 188 + src/shared/components/MarkdownEditor.js | 245 + src/shared/components/Modal.js | 79 + src/shared/components/Pagination.js | 152 + .../components/PasswordStrengthIndicator.js | 189 + src/shared/components/Select.js | 65 + src/shared/components/StatCard.js | 81 + src/shared/components/Table.js | 254 + src/shared/components/Textarea.js | 56 + src/shared/components/index.js | 27 + src/shared/lib/appConfig.js | 77 + src/shared/lib/dates.js | 240 + src/shared/lib/init.js | 109 + src/shared/lib/metadata/index.js | 114 + src/shared/styles/zen.css | 2 + src/shared/utils/currency.js | 45 + tailwind.config.js | 10 + tsup.config.js | 104 + 239 files changed, 48581 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 docs/INSTALL.md create mode 100644 docs/dev/GUIDE.md create mode 100644 docs/dev/REDACTION.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 src/core/api/dynamic-router.js create mode 100644 src/core/api/handlers/health.js create mode 100644 src/core/api/handlers/storage.js create mode 100644 src/core/api/handlers/users.js create mode 100644 src/core/api/handlers/version.js create mode 100644 src/core/api/index.js create mode 100644 src/core/api/nx-route.js create mode 100644 src/core/api/router.js create mode 100644 src/core/cron/index.js create mode 100644 src/core/database/cli.js create mode 100644 src/core/database/crud.js create mode 100644 src/core/database/db.js create mode 100644 src/core/database/index.js create mode 100644 src/core/database/init.js create mode 100644 src/core/email/index.js create mode 100644 src/core/email/templates/BaseLayout.jsx create mode 100644 src/core/email/templates/PasswordChangedEmail.jsx create mode 100644 src/core/email/templates/PasswordResetEmail.jsx create mode 100644 src/core/email/templates/VerificationEmail.jsx create mode 100644 src/core/email/templates/index.js create mode 100644 src/core/modules/client.js create mode 100644 src/core/modules/discovery.js create mode 100644 src/core/modules/index.js create mode 100644 src/core/modules/loader.js create mode 100644 src/core/modules/registry.js create mode 100644 src/core/payments/index.js create mode 100644 src/core/payments/stripe.js create mode 100644 src/core/pdf/index.js create mode 100644 src/core/storage/index.js create mode 100644 src/core/storage/utils.js create mode 100644 src/core/toast/Toast.js create mode 100644 src/core/toast/ToastContainer.js create mode 100644 src/core/toast/ToastContext.js create mode 100644 src/core/toast/index.js create mode 100644 src/features/admin/actions.js create mode 100644 src/features/admin/actions/statsActions.js create mode 100644 src/features/admin/components/AdminHeader.js create mode 100644 src/features/admin/components/AdminPages.js create mode 100644 src/features/admin/components/AdminPagesLayout.js create mode 100644 src/features/admin/components/AdminSidebar.js create mode 100644 src/features/admin/components/ThemeToggle.js create mode 100644 src/features/admin/components/index.js create mode 100644 src/features/admin/components/pages/DashboardPage.js create mode 100644 src/features/admin/components/pages/ProfilePage.js create mode 100644 src/features/admin/components/pages/UserEditPage.js create mode 100644 src/features/admin/components/pages/UsersPage.js create mode 100644 src/features/admin/index.js create mode 100644 src/features/admin/middleware/protect.js create mode 100644 src/features/admin/navigation.server.js create mode 100644 src/features/admin/page.js create mode 100644 src/features/admin/pages.js create mode 100644 src/features/auth/README-custom-login.md create mode 100644 src/features/auth/README-dashboard.md create mode 100644 src/features/auth/actions.js create mode 100644 src/features/auth/actions/authActions.js create mode 100644 src/features/auth/components/AccountSection.js create mode 100644 src/features/auth/components/AuthPages.js create mode 100644 src/features/auth/components/AuthPagesLayout.js create mode 100644 src/features/auth/components/UserAvatar.js create mode 100644 src/features/auth/components/UserMenu.js create mode 100644 src/features/auth/components/index.js create mode 100644 src/features/auth/components/pages/ConfirmEmailPage.js create mode 100644 src/features/auth/components/pages/ForgotPasswordPage.js create mode 100644 src/features/auth/components/pages/LoginPage.js create mode 100644 src/features/auth/components/pages/LogoutPage.js create mode 100644 src/features/auth/components/pages/RegisterPage.js create mode 100644 src/features/auth/components/pages/ResetPasswordPage.js create mode 100644 src/features/auth/components/useCurrentUser.js create mode 100644 src/features/auth/index.js create mode 100644 src/features/auth/lib/auth.js create mode 100644 src/features/auth/lib/email.js create mode 100644 src/features/auth/lib/password.js create mode 100644 src/features/auth/lib/rateLimit.js create mode 100644 src/features/auth/lib/session.js create mode 100644 src/features/auth/middleware/protect.js create mode 100644 src/features/auth/page.js create mode 100644 src/features/auth/pages.js create mode 100644 src/features/provider/ZenProvider.js create mode 100644 src/features/provider/index.js create mode 100644 src/features/setup/cli.js create mode 100644 src/features/setup/index.js create mode 100644 src/index.js create mode 100644 src/modules/PublicPagesClient.js create mode 100644 src/modules/PublicPagesLayout.js create mode 100644 src/modules/README.md create mode 100644 src/modules/clients/.env.example create mode 100644 src/modules/clients/INSTALL.md create mode 100644 src/modules/clients/admin/ClientCreatePage.js create mode 100644 src/modules/clients/admin/ClientEditPage.js create mode 100644 src/modules/clients/admin/ClientForm.js create mode 100644 src/modules/clients/admin/ClientsListPage.js create mode 100644 src/modules/clients/admin/index.js create mode 100644 src/modules/clients/api.js create mode 100644 src/modules/clients/crud.js create mode 100644 src/modules/clients/db.js create mode 100644 src/modules/clients/index.js create mode 100644 src/modules/clients/module.config.js create mode 100644 src/modules/index.js create mode 100644 src/modules/init.js create mode 100644 src/modules/invoice/.env.example create mode 100644 src/modules/invoice/GUIDE-client-dashboard.md create mode 100644 src/modules/invoice/README.md create mode 100644 src/modules/invoice/actions.js create mode 100644 src/modules/invoice/admin/InvoiceCreatePage.js create mode 100644 src/modules/invoice/admin/InvoiceEditPage.js create mode 100644 src/modules/invoice/admin/InvoicesListPage.js create mode 100644 src/modules/invoice/admin/index.js create mode 100644 src/modules/invoice/api.js create mode 100644 src/modules/invoice/categories/admin/CategoriesListPage.js create mode 100644 src/modules/invoice/categories/admin/CategoryCreatePage.js create mode 100644 src/modules/invoice/categories/admin/CategoryEditPage.js create mode 100644 src/modules/invoice/categories/admin/index.js create mode 100644 src/modules/invoice/categories/crud.js create mode 100644 src/modules/invoice/categories/db.js create mode 100644 src/modules/invoice/cron.config.js create mode 100644 src/modules/invoice/crud.js create mode 100644 src/modules/invoice/dashboard/ClientInvoicesSection.js create mode 100644 src/modules/invoice/dashboard/InvoicesWidget.js create mode 100644 src/modules/invoice/dashboard/RevenueWidget.js create mode 100644 src/modules/invoice/dashboard/index.js create mode 100644 src/modules/invoice/dashboard/statsActions.js create mode 100644 src/modules/invoice/db.js create mode 100644 src/modules/invoice/email/AdminOverdueNotificationEmail.jsx create mode 100644 src/modules/invoice/email/InvoiceOverdueEmail.jsx create mode 100644 src/modules/invoice/email/InvoicePaymentConfirmationEmail.jsx create mode 100644 src/modules/invoice/email/InvoiceReceiptEmail.jsx create mode 100644 src/modules/invoice/email/InvoiceReminderEmail.jsx create mode 100644 src/modules/invoice/email/index.js create mode 100644 src/modules/invoice/index.js create mode 100644 src/modules/invoice/interest.js create mode 100644 src/modules/invoice/items/admin/ItemCreatePage.js create mode 100644 src/modules/invoice/items/admin/ItemEditPage.js create mode 100644 src/modules/invoice/items/admin/ItemsListPage.js create mode 100644 src/modules/invoice/items/admin/index.js create mode 100644 src/modules/invoice/items/crud.js create mode 100644 src/modules/invoice/items/db.js create mode 100644 src/modules/invoice/metadata.js create mode 100644 src/modules/invoice/module.config.js create mode 100644 src/modules/invoice/pages/InvoicePDFViewerPage.js create mode 100644 src/modules/invoice/pages/InvoicePublicPages.js create mode 100644 src/modules/invoice/pages/PaymentPage.js create mode 100644 src/modules/invoice/pages/ReceiptPDFViewerPage.js create mode 100644 src/modules/invoice/pages/ThemeToggle.js create mode 100644 src/modules/invoice/pages/index.js create mode 100644 src/modules/invoice/pdf/InvoicePDFTemplate.jsx create mode 100644 src/modules/invoice/pdf/ReceiptPDFTemplate.jsx create mode 100644 src/modules/invoice/pdf/generatePDF.js create mode 100644 src/modules/invoice/recurrences/admin/RecurrenceCreatePage.js create mode 100644 src/modules/invoice/recurrences/admin/RecurrenceEditPage.js create mode 100644 src/modules/invoice/recurrences/admin/RecurrencesListPage.js create mode 100644 src/modules/invoice/recurrences/admin/index.js create mode 100644 src/modules/invoice/recurrences/crud.js create mode 100644 src/modules/invoice/recurrences/db.js create mode 100644 src/modules/invoice/recurrences/processor.js create mode 100644 src/modules/invoice/reminders.js create mode 100644 src/modules/invoice/transactions/admin/TransactionCreatePage.js create mode 100644 src/modules/invoice/transactions/admin/TransactionsListPage.js create mode 100644 src/modules/invoice/transactions/admin/index.js create mode 100644 src/modules/invoice/transactions/crud.js create mode 100644 src/modules/invoice/transactions/db.js create mode 100644 src/modules/modules.actions.js create mode 100644 src/modules/modules.metadata.js create mode 100644 src/modules/modules.pages.js create mode 100644 src/modules/modules.registry.js create mode 100644 src/modules/nuage/.env.example create mode 100644 src/modules/nuage/GUIDE-client-dashboard.md create mode 100644 src/modules/nuage/README.md create mode 100644 src/modules/nuage/actions.js create mode 100644 src/modules/nuage/admin/ExplorerPage.js create mode 100644 src/modules/nuage/admin/SharesPage.js create mode 100644 src/modules/nuage/admin/index.js create mode 100644 src/modules/nuage/api.js create mode 100644 src/modules/nuage/components/FileViewerModal.js create mode 100644 src/modules/nuage/components/NuageFileTable.js create mode 100644 src/modules/nuage/components/SharePanel.js create mode 100644 src/modules/nuage/crud.js create mode 100644 src/modules/nuage/dashboard/ClientNuageSection.js create mode 100644 src/modules/nuage/dashboard/index.js create mode 100644 src/modules/nuage/db.js create mode 100644 src/modules/nuage/email/NuageShareEmail.jsx create mode 100644 src/modules/nuage/email/index.js create mode 100644 src/modules/nuage/metadata.js create mode 100644 src/modules/nuage/module.config.js create mode 100644 src/modules/nuage/pages/NuagePublicPages.js create mode 100644 src/modules/nuage/pages/index.js create mode 100644 src/modules/page.js create mode 100644 src/modules/pages.js create mode 100644 src/modules/posts/.env.example create mode 100644 src/modules/posts/README.md create mode 100644 src/modules/posts/admin/PostCreatePage.js create mode 100644 src/modules/posts/admin/PostEditPage.js create mode 100644 src/modules/posts/admin/PostFormFields.js create mode 100644 src/modules/posts/admin/PostsIndexPage.js create mode 100644 src/modules/posts/admin/PostsListPage.js create mode 100644 src/modules/posts/api.js create mode 100644 src/modules/posts/categories/admin/CategoriesListPage.js create mode 100644 src/modules/posts/categories/admin/CategoryCreatePage.js create mode 100644 src/modules/posts/categories/admin/CategoryEditPage.js create mode 100644 src/modules/posts/categories/crud.js create mode 100644 src/modules/posts/config.js create mode 100644 src/modules/posts/crud.js create mode 100644 src/modules/posts/db.js create mode 100644 src/modules/posts/module.config.js create mode 100644 src/shared/Icons.js create mode 100644 src/shared/components/Badge.js create mode 100644 src/shared/components/Breadcrumb.js create mode 100644 src/shared/components/Button.js create mode 100644 src/shared/components/Card.js create mode 100644 src/shared/components/FilterTabs.js create mode 100644 src/shared/components/Input.js create mode 100644 src/shared/components/Loading.js create mode 100644 src/shared/components/LoadingState.js create mode 100644 src/shared/components/MarkdownEditor.js create mode 100644 src/shared/components/Modal.js create mode 100644 src/shared/components/Pagination.js create mode 100644 src/shared/components/PasswordStrengthIndicator.js create mode 100644 src/shared/components/Select.js create mode 100644 src/shared/components/StatCard.js create mode 100644 src/shared/components/Table.js create mode 100644 src/shared/components/Textarea.js create mode 100644 src/shared/components/index.js create mode 100644 src/shared/lib/appConfig.js create mode 100644 src/shared/lib/dates.js create mode 100644 src/shared/lib/init.js create mode 100644 src/shared/lib/metadata/index.js create mode 100644 src/shared/styles/zen.css create mode 100644 src/shared/utils/currency.js create mode 100644 tailwind.config.js create mode 100644 tsup.config.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..db73489 --- /dev/null +++ b/.env.example @@ -0,0 +1,47 @@ +# CORE +NEXT_PUBLIC_URL=http://localhost:3000 +NEXT_PUBLIC_URL_DEV=http://localhost:3000 +ZEN_NAME=ZEN +ZEN_DESCRIPTION= + +# CONFIG +ZEN_TIMEZONE=America/Toronto +ZEN_DATE_FORMAT=YYYY-MM-DD +ZEN_CURRENCY=CAD +ZEN_CURRENCY_SYMBOL=$ +ZEN_SUPPORT_EMAIL=support@exemple.com + +# DATABASE +ZEN_DATABASE_URL=postgres://USER:PASSWORD@HOST:PORT/postgres +# Used when NODE_ENV=development (falls back to ZEN_DATABASE_URL if unset) +ZEN_DATABASE_URL_DEV=postgres://USER:PASSWORD@HOST:PORT/postgres_dev + +# STORAGE (Cloudflare R2 for now) +ZEN_STORAGE_BUCKET=my-bucket-name +ZEN_STORAGE_REGION=your-account-id +ZEN_STORAGE_ACCESS_KEY= +ZEN_STORAGE_SECRET_KEY= + +# EMAIL +ZEN_EMAIL_RESEND_APIKEY= +ZEN_EMAIL_FROM_NAME="EXEMPLE" +ZEN_EMAIL_FROM_ADDRESS=app@exemple.com +ZEN_EMAIL_LOGO= +ZEN_EMAIL_LOGO_URL= + +# STRIPE +STRIPE_SECRET_KEY= +STRIPE_PUBLISHABLE_KEY= +STRIPE_WEBHOOK_SECRET= + +# AUTH SETTINGS +ZEN_AUTH_REDIRECT_AFTER_LOGIN=/admin +ZEN_AUTH_SESSION_COOKIE_NAME=zen_session + +# PUBLIC SETTINGS +ZEN_PUBLIC_LOGO_WHITE= +ZEN_PUBLIC_LOGO_BLACK= +ZEN_PUBLIC_LOGO_URL= + +# OTHERS +NEXT_TELEMETRY_DISABLED=1 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba466e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Build output +dist/ + +# Dependencies +node_modules/ + +# Environment variables +.env +.env.local +.env.*.local + +# Logs +*.log +npm-debug.log* + +# OS +.DS_Store + +# IDE +.vscode/ +.idea/ + diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..42aeb77 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@hykocx:registry=https://git.hyko.cx \ No newline at end of file diff --git a/README.md b/README.md index f6256da..3619bb4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ # zen -Un CMS construit sur l'essentiel, rien de plus, rien de moins. \ No newline at end of file +> [!WARNING] +> Ce projet est en développement actif et n'est pas encore prêt pour une utilisation en production. L'API, la structure et les fonctionnalités peuvent changer à tout moment. + +Un CMS construit sur l'essentiel, rien de plus, rien de moins. + +## Fonctionnalités + +- **Système de modules dynamiques** - Créez des modules sans modifier le code principal +- **Authentification** - Authentification et autorisation des utilisateurs intégrées +- **Tableau de bord** - Génération automatique d'interfaces d'administrations +- **Routeur API** - API RESTful avec authentification +- **Système d'emails** - Templates d'emails avec React Email +- **Stockage** - Stockage de fichiers compatible S3 +- **Paiements** - Intégration Stripe +- **Tâches planifiées** - Gestion des tâches programmées + +## Démarrage + +Pour les instructions d'installation et de configuration, voir [INSTALL.md](./docs/INSTALL.md). \ No newline at end of file diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 0000000..0ee9eba --- /dev/null +++ b/docs/INSTALL.md @@ -0,0 +1,61 @@ +# Installation + +## 1. Install the package + +```bash +npm install @hykocx/zen +``` + +## 2. Install the styles + +Add the following line to your `globals.css` file: + +```css +@import '@hykocx/zen/styles/zen.css'; +``` + +## 3. Add ZenProvider to your root layout + +Wrap your application with the `ZenProvider` in your root layout to enable toast notifications globally: + +```javascript +// app/layout.js +import './globals.css'; +import { ZenProvider } from '@hykocx/zen/provider'; + +export const metadata = { + title: 'My App', +}; + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + + ); +} +``` + +## 4. Configure the environment variables + +Check the [`.env.example`](.env.example) file for the required environment variables to add to your `.env` file. + +## 5. Initialize the database + +```bash +npx zen-db init +``` + +# Setup + +## Quick Setup + +You can create all required files with a single command: + +```bash +npx zen-setup init +``` \ No newline at end of file diff --git a/docs/dev/GUIDE.md b/docs/dev/GUIDE.md new file mode 100644 index 0000000..b7ee815 --- /dev/null +++ b/docs/dev/GUIDE.md @@ -0,0 +1,38 @@ +# GUIDE + +## Langue du code + +Tout ce qui est **code** est en **anglais**, sans exception : +- Noms de fichiers (sauf dossiers de routes Next.js, voir ci-dessous) +- Variables, fonctions, classes, composants +- Commentaires dans le code +- README.md +- Props, événements, constantes, types +- Git commit + +## Langue du contenu affiché + +Tout ce qui est **visible par l'utilisateur** est en **français** : +- Textes, titres, descriptions, labels +- Slugs et noms de dossiers qui correspondent à des routes URL +- Documentations + +## Messages de commit Git + +Tous les messages de commit doivent être rédigés en **anglais**, en suivant le format conventional commits : + +``` +(): +``` + +Types courants : `feat`, `fix`, `refactor`, `style`, `docs`, `test`, `chore` + +Exemples : +- `feat(auth): add OAuth2 login support` +- `fix(api): handle null response from payment gateway` +- `docs(guide): add git commit message conventions` +- `chore(deps): update dependencies` + +## Guide de rédaction + +Se référer à `REDACTION.md` avant de rédiger tout contenu textuel. diff --git a/docs/dev/REDACTION.md b/docs/dev/REDACTION.md new file mode 100644 index 0000000..7e8b4bb --- /dev/null +++ b/docs/dev/REDACTION.md @@ -0,0 +1,163 @@ +# GUIDE DE RÉDACTION +Dernière modification : 2026-04-12 + +## La voix de l'entreprise + +On parle comme une personne, pas comme une organisation. Direct, sans fioritures. + +On dit **"on"** — jamais "nous sommes heureux de", jamais "notre équipe d'experts". Une seule exception : quand on cite quelqu'un nommément, on peut utiliser "je". + +La technique est là parce qu'elle est utile, pas pour impressionner. Si une explication technique ne sert pas le lecteur, elle ne sert pas le texte. + +--- + +## Ce qu'on évite absolument + +**Les superlatifs vides** +*de premier plan, leader, de pointe, best-in-class, incontournable* + +**Les promesses floues** +*solutions sur mesure, accompagnement personnalisé, approche holistique* + +**Le corporate** +*nous nous engageons à, notre mission est de, dans une optique de, à cet effet* + +**La sur-promesse** +Si on ne peut pas le garantir, on ne l'écrit pas. Jamais. + +**Les métaphores usées** +*pont entre, clé en main, écosystème, synergies, à 360°* + +**Le tiret long (—)** +Reformuler la phrase plutôt que d'insérer une incise. + +**Les chevilles inutiles** +*Ainsi, En effet, Il convient de noter que, N'hésitez pas à* + +**Le passif sans raison** +Préférer la forme active. "On a livré le projet" plutôt que "le projet a été livré". + +--- + +## Formules qui marchent + +**Plutôt ça :** +> "On dit non quand c'est la bonne réponse." +> "On pense à comment les choses vont tenir dans six mois." +> "Ce n'est pas le bon projet pour nous. Voilà pourquoi." + +**Pas ça :** +> "Notre approche centrée client garantit des résultats optimaux." +> "Nous mettons notre expertise au service de vos ambitions." +> "Une équipe passionnée à votre écoute." + +**La règle d'or :** si ça pourrait figurer dans la communication d'un concurrent sans changer un mot, c'est à réécrire. + +--- + +## Structure des textes + +### Titres +Courts, affirmatifs, sans point. Une idée, pas une liste. Pas de question rhétorique. + +> ✓ "Zéro raccourci. Zéro compromis." +> ✓ "On livre. On reste." +> ✗ "Une approche rigoureuse pour des résultats durables" +> ✗ "Pourquoi nous choisir ?" + +### Corps de texte +Phrases courtes. Une idée par phrase. Maximum deux virgules par phrase — si on en compte plus, couper. + +Paragraphes de trois à quatre phrases maximum. Une ligne blanche entre chaque paragraphe. + +Pas de bullet points pour des concepts. Seulement pour des listes réelles (étapes, éléments techniques, options). + +### Appels à l'action +Concrets et à l'infinitif. Pas d'exclamation. + +> ✓ "Discuter de votre projet" +> ✓ "Voir comment on travaille" +> ✗ "Contactez-nous pour en savoir plus !" +> ✗ "Passez à l'action" + +--- + +## Selon le format + +### Site web +Chaque page a une seule idée principale. Le visiteur doit comprendre l'essentiel en dix secondes. Les détails viennent après, pour ceux qui cherchent. + +Pas de jargon en page d'accueil. Le jargon technique appartient aux pages de service, là où le lecteur s'y attend. + +### Courriels +Objet : concret, pas accrocheur. Dire ce dont il s'agit, pas promettre quelque chose. + +> ✓ "Proposition — Refonte infrastructure" +> ✗ "Une opportunité à ne pas manquer" + +Corps du courriel : aller droit au but dès la première phrase. Pas de "j'espère que ce message vous trouve bien." Si on doit écrire plus de quatre paragraphes, se demander si c'est le bon format. + +### Applications et interfaces +Chaque message, étiquette ou instruction doit avoir un seul sens possible. Pas de formulation qui force l'utilisateur à interpréter. + +Le ton reste humain, même dans un contexte technique. "Une erreur s'est produite" est mieux que "Erreur 403 — accès non autorisé" si le lecteur n'est pas développeur. + +Les messages de confirmation, d'erreur et d'aide suivent les mêmes règles que le reste : courts, directs, actifs. + +### Articles et billets +Un angle précis, pas un survol. Mieux vaut un article sur un problème concis que dix paragraphes vagues. + +Commencer par un fait, un constat ou une situation — pas par une définition. + +> ✓ "La plupart des migrations ratent pour la même raison : on sous-estime ce qui dépend de ce qu'on déplace." +> ✗ "La migration de données est un processus complexe qui nécessite une planification rigoureuse." + +Terminer sur ce qu'on retient, pas sur une invitation à nous contacter. + +### Propositions et soumissions +Pas de section "qui sommes-nous" en ouverture. Le client le sait déjà ou s'en fout pour l'instant. + +Commencer par le problème du client, tel qu'on l'a compris. S'il se reconnaît dans les deux premiers paragraphes, le reste a de la valeur. + +La solution vient après le diagnostic. Jamais avant. + +### Documents internes +Mêmes règles de clarté que pour les textes externes. Un document interne mal écrit crée autant de confusion qu'une mauvaise communication client. + +Titres de section descriptifs, pas génériques. "Décision retenue et raison" est mieux que "Résultats". + +Si un document dépasse deux pages, se demander si l'information ne serait pas mieux transmise autrement. + +### Réseaux sociaux +Un seul message par publication. Si on a besoin d'un fil de publications pour expliquer, c'est probablement un article. + +Le ton reste celui de l'entreprise — pas plus décontracté sous prétexte que c'est un réseau social. + +Pas de hashtags décoratifs. Seulement ceux qui servent la découverte du contenu. + +--- + +## Parler de ce qu'on fait + +Ne jamais affirmer qu'on est "les meilleurs" ou "différents des autres". Le montrer, pas le dire. + +Quand on parle de ce qu'on fait, on parle de situations concrètes. Pas de généralités. + +> ✓ "On a refusé un mandat parce que le calendrier ne tenait pas." +> ✗ "On valorise l'honnêteté dans nos relations clients." + +--- + +## Révision d'un texte + +Avant de publier ou d'envoyer, se poser ces questions : + +1. **Est-ce qu'un concurrent pourrait signer ce texte ?** Si oui, réécrire. +2. **Est-ce qu'on promet quelque chose qu'on ne contrôle pas ?** Retirer ou nuancer. +3. **Est-ce que chaque phrase dit quelque chose ?** Supprimer celles qui ne font que meubler. +4. **Est-ce que le lecteur sait quoi faire ou quoi penser après ?** Si non, préciser. +5. **A-t-on utilisé le mot "solution" ?** Le remplacer. Presque toujours. + +--- + +*Ce guide évolue. Si une formule sonne faux ou si un cas n'est pas couvert, on l'ajuste.* \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..32f2374 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7598 @@ +{ + "name": "@hykocx/zen", + "version": "1.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@hykocx/zen", + "version": "1.4.0", + "license": "GPL-3.0-only", + "dependencies": { + "@headlessui/react": "^2.0.0", + "@react-email/components": "^0.5.6", + "@react-pdf/renderer": "^4.3.1", + "dotenv": "^16.4.5", + "node-cron": "^3.0.3", + "pg": "^8.11.3", + "react-email": "^4.3.0", + "react-grid-layout": "^1.5.2", + "resend": "^3.2.0", + "stripe": "^14.0.0" + }, + "bin": { + "zen-db": "dist/core/database/cli.js", + "zen-setup": "dist/features/setup/cli.js" + }, + "devDependencies": { + "@tailwindcss/cli": "^4.2.1", + "@tailwindcss/postcss": "^4", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^4", + "tsup": "^8.0.0", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "next": ">=14.0.0", + "react": "^19.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "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": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "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/@headlessui/react": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.10.tgz", + "integrity": "sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "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, + "peer": 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" + ], + "peer": true, + "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" + ], + "peer": true, + "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" + ], + "peer": true, + "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" + ], + "peer": true, + "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" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "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, + "peer": 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" + ], + "peer": true, + "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" + ], + "peer": true, + "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" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "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/@next/env": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz", + "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==", + "license": "MIT", + "peer": true + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz", + "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz", + "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz", + "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz", + "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz", + "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz", + "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz", + "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz", + "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "license": "MIT" + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@react-aria/focus": { + "version": "3.21.5", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.5.tgz", + "integrity": "sha512-V18fwCyf8zqgJdpLQeDU5ZRNd9TeOfBbhLgmX77Zr5ae9XwaoJ1R3SFJG1wCJX60t34AW+aLZSEEK+saQElf3Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.1", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.27.1.tgz", + "integrity": "sha512-M3wLpTTmDflI0QGNK0PJNUaBXXfeBXue8ZxLMngfc1piHNiH4G5lUvWd9W14XVbqrSCVY8i8DfGrNYpyyZu0tw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.33.1.tgz", + "integrity": "sha512-kIx1Sj6bbAT0pdqCegHuPanR9zrLn5zMRiM7LN12rgRf55S19ptd9g3ncahArifYTRkfEU9VIn+q0HjfMqS9/w==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-email/body": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.1.0.tgz", + "integrity": "sha512-o1bcSAmDYNNHECbkeyceCVPGmVsYvT+O3sSO/Ct7apKUu3JphTi31hu+0Nwqr/pgV5QFqdoT5vdS3SW5DJFHgQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/button": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.0.tgz", + "integrity": "sha512-8i+v6cMxr2emz4ihCrRiYJPp2/sdYsNNsBzXStlcA+/B9Umpm5Jj3WJKYpgTPM+aeyiqlG/MMI1AucnBm4f1oQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/code-block": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.1.0.tgz", + "integrity": "sha512-jSpHFsgqnQXxDIssE4gvmdtFncaFQz5D6e22BnVjcCPk/udK+0A9jRwGFEG8JD2si9ZXBmU4WsuqQEczuZn4ww==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/code-inline": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.5.tgz", + "integrity": "sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/column": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.13.tgz", + "integrity": "sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/components": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.5.7.tgz", + "integrity": "sha512-ECyVoyDcev2FSQ7C0buXaIJ0+6MRDXNUbCOZwBRrlLdCCRjap2b4+MHrYSTXFzo5kqfjjRoyo/2PbJXFQni67g==", + "license": "MIT", + "dependencies": { + "@react-email/body": "0.1.0", + "@react-email/button": "0.2.0", + "@react-email/code-block": "0.1.0", + "@react-email/code-inline": "0.0.5", + "@react-email/column": "0.0.13", + "@react-email/container": "0.0.15", + "@react-email/font": "0.0.9", + "@react-email/head": "0.0.12", + "@react-email/heading": "0.0.15", + "@react-email/hr": "0.0.11", + "@react-email/html": "0.0.11", + "@react-email/img": "0.0.11", + "@react-email/link": "0.0.12", + "@react-email/markdown": "0.0.16", + "@react-email/preview": "0.0.13", + "@react-email/render": "1.4.0", + "@react-email/row": "0.0.12", + "@react-email/section": "0.0.16", + "@react-email/tailwind": "1.2.2", + "@react-email/text": "0.1.5" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/container": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.15.tgz", + "integrity": "sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/font": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.9.tgz", + "integrity": "sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/head": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.12.tgz", + "integrity": "sha512-X2Ii6dDFMF+D4niNwMAHbTkeCjlYYnMsd7edXOsi0JByxt9wNyZ9EnhFiBoQdqkE+SMDcu8TlNNttMrf5sJeMA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/heading": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.15.tgz", + "integrity": "sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/hr": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.11.tgz", + "integrity": "sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/html": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.11.tgz", + "integrity": "sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/img": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.11.tgz", + "integrity": "sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/link": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.12.tgz", + "integrity": "sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/markdown": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.16.tgz", + "integrity": "sha512-KSUHmoBMYhvc6iGwlIDkm0DRGbGQ824iNjLMCJsBVUoKHGQYs7F/N3b1tnS1YzRUX+GwHIexSsHuIUEi1m+8OQ==", + "license": "MIT", + "dependencies": { + "marked": "^15.0.12" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/preview": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.13.tgz", + "integrity": "sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/render": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.4.0.tgz", + "integrity": "sha512-ZtJ3noggIvW1ZAryoui95KJENKdCzLmN5F7hyZY1F/17B1vwzuxHB7YkuCg0QqHjDivc5axqYEYdIOw4JIQdUw==", + "license": "MIT", + "dependencies": { + "html-to-text": "^9.0.5", + "prettier": "^3.5.3", + "react-promise-suspense": "^0.3.4" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/row": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.12.tgz", + "integrity": "sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/section": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.16.tgz", + "integrity": "sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/tailwind": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.2.2.tgz", + "integrity": "sha512-heO9Khaqxm6Ulm6p7HQ9h01oiiLRrZuuEQuYds/O7Iyp3c58sMVHZGIxiRXO/kSs857NZQycpjewEVKF3jhNTw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/text": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.5.tgz", + "integrity": "sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-pdf/fns": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@react-pdf/fns/-/fns-3.1.3.tgz", + "integrity": "sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w==", + "license": "MIT" + }, + "node_modules/@react-pdf/font": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@react-pdf/font/-/font-4.0.6.tgz", + "integrity": "sha512-1RxR/hTyZcbgjESUjrMms574xuS9PLB4ovqQx6jvgdrIHXUyeUtSH6i3Szw1qVfUnA9MfaEm1FBuydQeJD39BQ==", + "license": "MIT", + "dependencies": { + "@react-pdf/pdfkit": "^5.0.0", + "@react-pdf/types": "^2.10.0", + "fontkit": "^2.0.2", + "is-url": "^1.2.4" + } + }, + "node_modules/@react-pdf/image": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@react-pdf/image/-/image-3.0.4.tgz", + "integrity": "sha512-z0ogVQE0bKqgXQ5smgzIU857rLV7bMgVdrYsu3UfXDDLSzI7QPvzf6MFTFllX6Dx2rcsF13E01dqKPtJEM799g==", + "license": "MIT", + "dependencies": { + "@react-pdf/png-js": "^3.0.0", + "jay-peg": "^1.1.1" + } + }, + "node_modules/@react-pdf/layout": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@react-pdf/layout/-/layout-4.5.1.tgz", + "integrity": "sha512-1V8ssgg9FHVsmvuCKmp7TWoUiPGgxAR2cgyvdcao8UQm7emWB7rP1o4CieHH56kgZyXXbwWqQAmmtgvcju+xfA==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.3", + "@react-pdf/image": "^3.0.4", + "@react-pdf/primitives": "^4.2.0", + "@react-pdf/stylesheet": "^6.1.4", + "@react-pdf/textkit": "^6.2.0", + "@react-pdf/types": "^2.10.0", + "emoji-regex-xs": "^1.0.0", + "queue": "^6.0.1", + "yoga-layout": "^3.2.1" + } + }, + "node_modules/@react-pdf/pdfkit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/pdfkit/-/pdfkit-5.0.0.tgz", + "integrity": "sha512-FcQBWGtfhMGuOB0G3NcnF/cBq/JnFVs22i1tuafiT1XlmG6KjCxgTGng5bVh+b9RtTuwNpUGmCtB6CmG6B4ZVA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@noble/ciphers": "^1.0.0", + "@noble/hashes": "^1.6.0", + "@react-pdf/png-js": "^3.0.0", + "browserify-zlib": "^0.2.0", + "fontkit": "^2.0.2", + "jay-peg": "^1.1.1", + "js-md5": "^0.8.3", + "linebreak": "^1.1.0", + "vite-compatible-readable-stream": "^3.6.1" + } + }, + "node_modules/@react-pdf/png-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/png-js/-/png-js-3.0.0.tgz", + "integrity": "sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==", + "license": "MIT", + "dependencies": { + "browserify-zlib": "^0.2.0" + } + }, + "node_modules/@react-pdf/primitives": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@react-pdf/primitives/-/primitives-4.2.0.tgz", + "integrity": "sha512-onlXLcA6SpsD7SX9HOyt55qdRRJCfauegPlo4ZNw0hA/IipaZTbT9MJliWKtEXm03ibGxAQyp/BgTuXm91fo0A==", + "license": "MIT" + }, + "node_modules/@react-pdf/reconciler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-pdf/reconciler/-/reconciler-2.0.0.tgz", + "integrity": "sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "scheduler": "0.25.0-rc-603e6108-20241029" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-pdf/render": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@react-pdf/render/-/render-4.4.1.tgz", + "integrity": "sha512-TBaEw6F+IBI4oVHUF7LL2OJX87unRrk6r7mkEmgjehN9BV5LF53I8CzVtdAchuO1+YhvE4MoMzkNelA+X2luRA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.3", + "@react-pdf/primitives": "^4.2.0", + "@react-pdf/textkit": "^6.2.0", + "@react-pdf/types": "^2.10.0", + "abs-svg-path": "^0.1.1", + "color-string": "^1.9.1", + "normalize-svg-path": "^1.1.0", + "parse-svg-path": "^0.1.2", + "svg-arc-to-cubic-bezier": "^3.2.0" + } + }, + "node_modules/@react-pdf/renderer": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@react-pdf/renderer/-/renderer-4.4.1.tgz", + "integrity": "sha512-mK7xyCdDUagO1kg8jraad3aUzdVAGBru08qyjjp8FMhGsh4BcuPGa0SycQ8Pv8EDEdyEOfmiE+XI1sBybSLwaQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@react-pdf/fns": "3.1.3", + "@react-pdf/font": "^4.0.6", + "@react-pdf/layout": "^4.5.1", + "@react-pdf/pdfkit": "^5.0.0", + "@react-pdf/primitives": "^4.2.0", + "@react-pdf/reconciler": "^2.0.0", + "@react-pdf/render": "^4.4.1", + "@react-pdf/types": "^2.10.0", + "events": "^3.3.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "queue": "^6.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-pdf/stylesheet": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@react-pdf/stylesheet/-/stylesheet-6.1.4.tgz", + "integrity": "sha512-jiwovO7lUwgccAh3JbVcXnh90AiSKZetdz2ETcWsKApPPLzLUzPkEs6wCVvZqh3lcGOAPFV3AfdMkFnLwv1ryg==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.3", + "@react-pdf/types": "^2.10.0", + "color-string": "^1.9.1", + "hsl-to-hex": "^1.0.0", + "media-engine": "^1.0.3", + "postcss-value-parser": "^4.1.0" + } + }, + "node_modules/@react-pdf/textkit": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@react-pdf/textkit/-/textkit-6.2.0.tgz", + "integrity": "sha512-0B22Kue/ALHiEcYNbrx2BdkpHPTq2j3u2xmAyCnf3XJbTyANjljJjtWRohkVLQKqOlieD88BvmQt7OeWLj+ZYg==", + "license": "MIT", + "dependencies": { + "@react-pdf/fns": "3.1.3", + "bidi-js": "^1.0.2", + "hyphen": "^1.6.4", + "unicode-properties": "^1.4.1" + } + }, + "node_modules/@react-pdf/types": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@react-pdf/types/-/types-2.10.0.tgz", + "integrity": "sha512-iz0NusqQ/9ZHQirWhJqOaxY1UkpvuNkEDtH4/SPCnhZJKBO/IhlFLFHuzbHkmWByBoX6X3m8GCc2b/1QH6QNlA==", + "license": "MIT", + "dependencies": { + "@react-pdf/font": "^4.0.6", + "@react-pdf/primitives": "^4.2.0", + "@react-pdf/stylesheet": "^6.1.4" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.11.0.tgz", + "integrity": "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.33.1.tgz", + "integrity": "sha512-oJHtjvLG43VjwemQDadlR5g/8VepK56B/xKO2XORPHt9zlW6IZs3tZrYlvH29BMvoqC7RtE7E5UjgbnbFtDGag==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/cli": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.2.tgz", + "integrity": "sha512-iJS+8kAFZ8HPqnh0O5DHCLjo4L6dD97DBQEkrhfSO4V96xeefUus2jqsBs1dUMt3OU9Ks4qIkiY0mpL5UW+4LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "enhanced-resolve": "^5.19.0", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.2.2" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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" + ], + "dev": true, + "libc": [ + "musl" + ], + "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" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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" + ], + "dev": true, + "libc": [ + "musl" + ], + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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==", + "dev": true, + "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/@tanstack/react-virtual": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz", + "integrity": "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.23" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "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/@tanstack/virtual-core": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", + "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/abs-svg-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", + "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "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/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz", + "integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "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.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "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/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "peer": true + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "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/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debounce": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", + "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.335", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz", + "integrity": "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, + "node_modules/engine.io": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz", + "integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", + "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hsl-to-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz", + "integrity": "sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==", + "license": "MIT", + "dependencies": { + "hsl-to-rgb-for-reals": "^1.1.0" + } + }, + "node_modules/hsl-to-rgb-for-reals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz", + "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==", + "license": "ISC" + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/hyphen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.14.1.tgz", + "integrity": "sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw==", + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jay-peg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz", + "integrity": "sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==", + "license": "MIT", + "dependencies": { + "restructure": "^3.0.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/js-beautify/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-beautify/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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" + ], + "dev": true, + "libc": [ + "musl" + ], + "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" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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" + ], + "dev": true, + "libc": [ + "musl" + ], + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz", + "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-engine": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", + "integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "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": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", + "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@next/env": "16.2.3", + "@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.3", + "@next/swc-darwin-x64": "16.2.3", + "@next/swc-linux-arm64-gnu": "16.2.3", + "@next/swc-linux-arm64-musl": "16.2.3", + "@next/swc-linux-x64-gnu": "16.2.3", + "@next/swc-linux-x64-musl": "16.2.3", + "@next/swc-win32-arm64-msvc": "16.2.3", + "@next/swc-win32-x64-msvc": "16.2.3", + "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/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", + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, + "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", + "peer": true, + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-svg-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", + "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", + "license": "MIT", + "dependencies": { + "svg-arc-to-cubic-bezier": "^3.0.0" + } + }, + "node_modules/nypm": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.0.tgz", + "integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "pathe": "^2.0.3", + "pkg-types": "^2.0.0", + "tinyexec": "^0.3.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-svg-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", + "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", + "license": "MIT" + }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "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-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "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/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prettier": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz", + "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-dom/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/react-draggable": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-email": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.3.2.tgz", + "integrity": "sha512-WaZcnv9OAIRULY236zDRdk+8r511ooJGH5UOb7FnVsV33hGPI+l5aIZ6drVjXi4QrlLTmLm8PsYvmXRSv31MPA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/traverse": "^7.27.0", + "chokidar": "^4.0.3", + "commander": "^13.0.0", + "debounce": "^2.0.0", + "esbuild": "^0.25.0", + "glob": "^11.0.0", + "jiti": "2.4.2", + "log-symbols": "^7.0.0", + "mime-types": "^3.0.0", + "normalize-path": "^3.0.0", + "nypm": "0.6.0", + "ora": "^8.0.0", + "prompts": "2.4.2", + "socket.io": "^4.8.1", + "tsconfig-paths": "4.2.0" + }, + "bin": { + "email": "dist/index.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/react-email/node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/react-grid-layout": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.3.tgz", + "integrity": "sha512-KaG6IbjD6fYhagUtIvOzhftXG+ViKZjCjADe86X1KHl7C/dsBN2z0mi14nbvZKTkp0RKiil9RPcJBgq3LnoA8g==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "fast-equals": "^4.0.3", + "prop-types": "^15.8.1", + "react-draggable": "^4.4.6", + "react-resizable": "^3.0.5", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-promise-suspense": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", + "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "license": "MIT" + }, + "node_modules/react-resizable": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.1.3.tgz", + "integrity": "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw==", + "license": "MIT", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.5.0" + }, + "peerDependencies": { + "react": ">= 16.3", + "react-dom": ">= 16.3" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resend": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-3.5.0.tgz", + "integrity": "sha512-bKu4LhXSecP6krvhfDzyDESApYdNfjirD5kykkT1xO0Cj9TKSiGh5Void4pGTs3Am+inSnp4dg0B5XzdwHBJOQ==", + "license": "MIT", + "dependencies": { + "@react-email/render": "0.0.16" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/resend/node_modules/@react-email/render": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.16.tgz", + "integrity": "sha512-wDaMy27xAq1cJHtSFptp0DTKPuV2GYhloqia95ub/DH9Dea1aWYsbdM918MOc/b/HvVS3w1z8DWzfAk13bGStQ==", + "license": "MIT", + "dependencies": { + "html-to-text": "9.0.5", + "js-beautify": "^1.14.11", + "react-promise-suspense": "0.3.4" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/resend/node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resend/node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/resend/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.25.0-rc-603e6108-20241029", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz", + "integrity": "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==", + "license": "MIT" + }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "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, + "peer": 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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "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==", + "dev": true, + "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/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/stripe": { + "version": "14.25.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.25.0.tgz", + "integrity": "sha512-wQS3GNMofCXwH8TSje8E1SE8zr6ODiGtHQgPtO95p9Mb4FhKC9jvXR2NUTpZ9ZINlckJcFidCmaTFV4P6vsb9g==", + "license": "MIT", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "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", + "peer": true, + "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/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", + "license": "ISC" + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsup/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "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==", + "dev": true, + "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-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite-compatible-readable-stream": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz", + "integrity": "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4df76b7 --- /dev/null +++ b/package.json @@ -0,0 +1,172 @@ +{ + "name": "@hykocx/zen", + "version": "1.3.0", + "description": "Un CMS construit sur l'essentiel, rien de plus, rien de moins.", + "repository": { + "type": "git", + "url": "https://git.hyko.cx/hykocx/zen.git" + }, + "publishConfig": { + "registry": "https://git.hyko.cx/" + }, + "license": "GPL-3.0-only", + "author": "Hyko", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "files": [ + "dist" + ], + "scripts": { + "build": "tsup && npm run build:css", + "build:css": "mkdir -p ./dist/shared/styles && cp ./src/shared/styles/zen.css ./dist/shared/styles/zen.css", + "prepublishOnly": "npm run build" + }, + "bin": { + "zen-db": "./dist/core/database/cli.js", + "zen-setup": "./dist/features/setup/cli.js" + }, + "dependencies": { + "@headlessui/react": "^2.0.0", + "@react-email/components": "^0.5.6", + "@react-pdf/renderer": "^4.3.1", + "dotenv": "^16.4.5", + "node-cron": "^3.0.3", + "pg": "^8.11.3", + "react-email": "^4.3.0", + "react-grid-layout": "^1.5.2", + "resend": "^3.2.0", + "stripe": "^14.0.0" + }, + "devDependencies": { + "@tailwindcss/cli": "^4.2.1", + "@tailwindcss/postcss": "^4", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwindcss": "^4", + "tsup": "^8.0.0", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "next": ">=14.0.0", + "react": "^19.0.0" + }, + "exports": { + ".": { + "import": "./dist/index.js" + }, + "./auth": { + "import": "./dist/features/auth/index.js" + }, + "./auth/actions": { + "import": "./dist/features/auth/actions.js" + }, + "./auth/pages": { + "import": "./dist/features/auth/pages.js" + }, + "./auth/page": { + "import": "./dist/features/auth/page.js" + }, + "./auth/components": { + "import": "./dist/features/auth/components/index.js" + }, + "./admin": { + "import": "./dist/features/admin/index.js" + }, + "./admin/actions": { + "import": "./dist/features/admin/actions.js" + }, + "./admin/navigation": { + "import": "./dist/features/admin/navigation.server.js" + }, + "./admin/pages": { + "import": "./dist/features/admin/pages.js" + }, + "./admin/page": { + "import": "./dist/features/admin/page.js" + }, + "./api": { + "import": "./dist/core/api/index.js" + }, + "./zen/api": { + "import": "./dist/core/api/nx-route.js" + }, + "./database": { + "import": "./dist/core/database/index.js" + }, + "./storage": { + "import": "./dist/core/storage/index.js" + }, + "./email": { + "import": "./dist/core/email/index.js" + }, + "./email/templates": { + "import": "./dist/core/email/templates/index.js" + }, + "./cron": { + "import": "./dist/core/cron/index.js" + }, + "./stripe": { + "import": "./dist/core/payments/stripe.js" + }, + "./payments": { + "import": "./dist/core/payments/index.js" + }, + "./pdf": { + "import": "./dist/core/pdf/index.js" + }, + "./toast": { + "import": "./dist/core/toast/index.js" + }, + "./provider": { + "import": "./dist/features/provider/index.js" + }, + "./setup": { + "import": "./dist/features/setup/index.js" + }, + "./core/modules": { + "import": "./dist/core/modules/index.js" + }, + "./core/modules/client": { + "import": "./dist/core/modules/client.js" + }, + "./modules": { + "import": "./dist/modules/index.js" + }, + "./modules/pages": { + "import": "./dist/modules/pages.js" + }, + "./modules/actions": { + "import": "./dist/modules/modules.actions.js" + }, + "./modules/posts/crud": { + "import": "./dist/modules/posts/crud.js" + }, + "./modules/metadata": { + "import": "./dist/modules/modules.metadata.js" + }, + "./invoice/dashboard": { + "import": "./dist/modules/invoice/dashboard/index.js" + }, + "./nuage/dashboard": { + "import": "./dist/modules/nuage/dashboard/index.js" + }, + "./modules/page": { + "import": "./dist/modules/page.js" + }, + "./lib/metadata": { + "import": "./dist/shared/lib/metadata/index.js" + }, + "./components": { + "import": "./dist/shared/components/index.js" + }, + "./icons": { + "import": "./dist/shared/Icons.js" + }, + "./styles/zen.css": { + "default": "./dist/shared/styles/zen.css" + } + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src/core/api/dynamic-router.js b/src/core/api/dynamic-router.js new file mode 100644 index 0000000..ee6eba9 --- /dev/null +++ b/src/core/api/dynamic-router.js @@ -0,0 +1,303 @@ +/** + * Dynamic API Router + * Routes incoming requests to appropriate handlers + * Supports both core routes and dynamically discovered module routes + */ + +import { validateSession } from '../../features/auth/lib/session.js'; +import { cookies } from 'next/headers'; +import { getSessionCookieName } from '../../shared/lib/appConfig.js'; + +// Core handlers +import { handleHealth } from './handlers/health.js'; +import { handleVersion } from './handlers/version.js'; +import { + handleGetCurrentUser, + handleGetUserById, + handleListUsers, + handleUpdateProfile, + handleUpdateUserById, + handleUploadProfilePicture, + handleDeleteProfilePicture +} from './handlers/users.js'; +import { handleGetFile } from './handlers/storage.js'; +import updatesHandler from './handlers/updates.js'; + +// Get cookie name from environment or use default +const COOKIE_NAME = getSessionCookieName(); + +/** + * Check if user is authenticated + * @param {Request} request - The request object + * @returns {Promise} Session object if authenticated + * @throws {Error} If not authenticated + */ +async function requireAuth(request) { + const cookieStore = await cookies(); + const sessionToken = cookieStore.get(COOKIE_NAME)?.value; + + if (!sessionToken) { + throw new Error('Unauthorized'); + } + + const session = await validateSession(sessionToken); + + if (!session || !session.user) { + throw new Error('Unauthorized'); + } + + return session; +} + +/** + * Check if user is authenticated and is admin + * @param {Request} request - The request object + * @returns {Promise} Session object if authenticated and admin + * @throws {Error} If not authenticated or not admin + */ +async function requireAdmin(request) { + const session = await requireAuth(request); + + if (session.user.role !== 'admin') { + throw new Error('Admin access required'); + } + + return session; +} + +/** + * Route an API request to the appropriate handler + * @param {Request} request - The incoming request + * @param {string[]} path - The path segments after /zen/api/ + * @returns {Promise} - The response data + */ +export async function routeRequest(request, path) { + const method = request.method; + + // Try core routes first + const coreResult = await routeCoreRequest(request, path, method); + if (coreResult !== null) { + return coreResult; + } + + // Try module routes + const moduleResult = await routeModuleRequest(request, path, method); + if (moduleResult !== null) { + return moduleResult; + } + + // No matching route + return { + error: 'Not Found', + message: `No handler found for ${method} ${path.join('/')}`, + path: path + }; +} + +/** + * Route core (non-module) requests + * @param {Request} request - The incoming request + * @param {string[]} path - The path segments + * @param {string} method - HTTP method + * @returns {Promise} Response or null if no match + */ +async function routeCoreRequest(request, path, method) { + // Health check endpoint + if (path[0] === 'health' && method === 'GET') { + return await handleHealth(); + } + + // Version endpoint + if (path[0] === 'version' && method === 'GET') { + return await handleVersion(); + } + + // Updates endpoint + if (path[0] === 'updates' && method === 'GET') { + return await updatesHandler(request); + } + + // Storage endpoint - serve files securely + if (path[0] === 'storage' && method === 'GET') { + const fileKey = path.slice(1).join('/'); + if (!fileKey) { + return { + error: 'Bad Request', + message: 'File path is required' + }; + } + return await handleGetFile(request, fileKey); + } + + // User endpoints + if (path[0] === 'users') { + // GET /zen/api/users - List all users (admin only) + if (path.length === 1 && method === 'GET') { + return await handleListUsers(request); + } + + // GET /zen/api/users/me - Get current user + if (path[1] === 'me' && method === 'GET') { + return await handleGetCurrentUser(request); + } + + // PUT /zen/api/users/profile - Update current user profile + if (path[1] === 'profile' && method === 'PUT') { + return await handleUpdateProfile(request); + } + + // POST /zen/api/users/profile/picture - Upload profile picture + if (path[1] === 'profile' && path[2] === 'picture' && method === 'POST') { + return await handleUploadProfilePicture(request); + } + + // DELETE /zen/api/users/profile/picture - Delete profile picture + if (path[1] === 'profile' && path[2] === 'picture' && method === 'DELETE') { + return await handleDeleteProfilePicture(request); + } + + // GET /zen/api/users/:id - Get user by ID (admin only) + if (path[1] && path[1] !== 'me' && path[1] !== 'profile' && method === 'GET') { + return await handleGetUserById(request, path[1]); + } + + // PUT /zen/api/users/:id - Update user by ID (admin only) + if (path[1] && path[1] !== 'me' && path[1] !== 'profile' && method === 'PUT') { + return await handleUpdateUserById(request, path[1]); + } + } + + return null; +} + +/** + * Route module requests + * @param {Request} request - The incoming request + * @param {string[]} path - The path segments + * @param {string} method - HTTP method + * @returns {Promise} Response or null if no match + */ +async function routeModuleRequest(request, path, method) { + try { + // Import module registry + const { getAllApiRoutes } = await import('../modules/registry.js'); + const routes = getAllApiRoutes(); + + // Convert path array to path string + const pathString = '/' + path.join('/'); + + // Find matching route + for (const route of routes) { + if (matchRoute(route.path, pathString) && route.method === method) { + // Check authentication + try { + if (route.auth === 'admin') { + await requireAdmin(request); + } else if (route.auth === 'user' || route.auth === 'auth') { + await requireAuth(request); + } + // 'public' or undefined means no auth required + + // Call the handler + if (typeof route.handler === 'function') { + // Extract path parameters + const params = extractPathParams(route.path, pathString); + return await route.handler(request, params); + } + } catch (authError) { + return { + success: false, + error: authError.message + }; + } + } + } + + return null; + } catch (error) { + console.error('[Dynamic Router] Error routing module request:', error); + return null; + } +} + +/** + * Match a route pattern against a path + * Supports path parameters like :id + * @param {string} pattern - Route pattern (e.g., '/admin/invoices/:id') + * @param {string} path - Actual path (e.g., '/admin/invoices/123') + * @returns {boolean} True if matches + */ +function matchRoute(pattern, path) { + const patternParts = pattern.split('/').filter(Boolean); + const pathParts = path.split('/').filter(Boolean); + + if (patternParts.length !== pathParts.length) { + return false; + } + + for (let i = 0; i < patternParts.length; i++) { + const patternPart = patternParts[i]; + const pathPart = pathParts[i]; + + // Skip parameter parts (they match anything) + if (patternPart.startsWith(':')) { + continue; + } + + if (patternPart !== pathPart) { + return false; + } + } + + return true; +} + +/** + * Extract path parameters from a path + * @param {string} pattern - Route pattern (e.g., '/admin/invoices/:id') + * @param {string} path - Actual path (e.g., '/admin/invoices/123') + * @returns {Object} Path parameters + */ +function extractPathParams(pattern, path) { + const params = {}; + const patternParts = pattern.split('/').filter(Boolean); + const pathParts = path.split('/').filter(Boolean); + + for (let i = 0; i < patternParts.length; i++) { + const patternPart = patternParts[i]; + + if (patternPart.startsWith(':')) { + const paramName = patternPart.slice(1); + params[paramName] = pathParts[i]; + } + } + + return params; +} + +/** + * Get the HTTP status code based on the response + * @param {Object} response - The response object + * @returns {number} - HTTP status code + */ +export function getStatusCode(response) { + if (response.error) { + switch (response.error) { + case 'Unauthorized': + return 401; + case 'Forbidden': + case 'Admin access required': + return 403; + case 'Not Found': + return 404; + case 'Bad Request': + return 400; + default: + return 500; + } + } + return 200; +} + +// Export auth helpers for use in module handlers +export { requireAuth, requireAdmin }; diff --git a/src/core/api/handlers/health.js b/src/core/api/handlers/health.js new file mode 100644 index 0000000..c26b383 --- /dev/null +++ b/src/core/api/handlers/health.js @@ -0,0 +1,13 @@ +/** + * Health Check Handler + * Returns the status of the API and basic system information + */ + +export async function handleHealth() { + return { + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + version: process.env.npm_package_version || '0.1.0' + }; +} diff --git a/src/core/api/handlers/storage.js b/src/core/api/handlers/storage.js new file mode 100644 index 0000000..908421f --- /dev/null +++ b/src/core/api/handlers/storage.js @@ -0,0 +1,127 @@ +/** + * Storage API Handlers + * Handles secure file access + */ + +import { validateSession } from '../../../features/auth/lib/session.js'; +import { cookies } from 'next/headers'; +import { getSessionCookieName } from '../../../shared/lib/appConfig.js'; +import { getFile } from '@hykocx/zen/storage'; + +// Get cookie name from environment or use default +const COOKIE_NAME = getSessionCookieName(); + +/** + * Serve a file from storage with security validation + * @param {Request} request - The request object + * @param {string} fileKey - The file key/path in storage + * @returns {Promise} File response or error object + */ +export async function handleGetFile(request, fileKey) { + try { + const pathParts = fileKey.split('/'); + + // Blog images: public read (no auth) for site integration + if (pathParts[0] === 'blog') { + const result = await getFile(fileKey); + if (!result.success) { + if (result.error.includes('NoSuchKey') || result.error.includes('not found')) { + return { error: 'Not Found', message: 'File not found' }; + } + return { error: 'Internal Server Error', message: result.error || 'Failed to retrieve file' }; + } + return { + success: true, + file: { + body: result.data.body, + contentType: result.data.contentType, + contentLength: result.data.contentLength, + lastModified: result.data.lastModified + } + }; + } + + // Require auth for other paths + const cookieStore = await cookies(); + const sessionToken = cookieStore.get(COOKIE_NAME)?.value; + + if (!sessionToken) { + return { + error: 'Unauthorized', + message: 'Authentication required to access files' + }; + } + + const session = await validateSession(sessionToken); + + if (!session) { + return { + error: 'Unauthorized', + message: 'Invalid or expired session' + }; + } + + // Security validation based on file path + if (pathParts[0] === 'users') { + // User files: users/{userId}/{category}/{filename} + const userId = pathParts[1]; + + // Users can only access their own files, unless they're admin + if (session.user.id !== userId && session.user.role !== 'admin') { + return { + error: 'Forbidden', + message: 'You do not have permission to access this file' + }; + } + } else if (pathParts[0] === 'organizations') { + // Organization files: organizations/{orgId}/{category}/{filename} + // For now, only admins can access organization files + if (session.user.role !== 'admin') { + return { + error: 'Forbidden', + message: 'Admin access required for organization files' + }; + } + } else { + // Unknown file path pattern - deny by default + return { + error: 'Forbidden', + message: 'Invalid file path' + }; + } + + // Get file from storage + const result = await getFile(fileKey); + + if (!result.success) { + if (result.error.includes('NoSuchKey') || result.error.includes('not found')) { + return { + error: 'Not Found', + message: 'File not found' + }; + } + return { + error: 'Internal Server Error', + message: result.error || 'Failed to retrieve file' + }; + } + + // Return the file data with proper headers + return { + success: true, + file: { + body: result.data.body, + contentType: result.data.contentType, + contentLength: result.data.contentLength, + lastModified: result.data.lastModified + } + }; + } catch (error) { + console.error('[ZEN STORAGE] Error serving file:', error); + return { + error: 'Internal Server Error', + message: error.message || 'Failed to retrieve file' + }; + } +} + diff --git a/src/core/api/handlers/users.js b/src/core/api/handlers/users.js new file mode 100644 index 0000000..d78ebbd --- /dev/null +++ b/src/core/api/handlers/users.js @@ -0,0 +1,568 @@ +/** + * Users API Handlers + * Handles user-related API endpoints + */ + +import { validateSession } from '../../../features/auth/lib/session.js'; +import { cookies } from 'next/headers'; +import { query, updateById } from '@hykocx/zen/database'; +import { getSessionCookieName, getModulesConfig } from '../../../shared/lib/appConfig.js'; +import { updateUser } from '../../../features/auth/lib/auth.js'; +import { uploadImage, deleteFile, generateUniqueFilename, generateUserFilePath, FILE_TYPE_PRESETS, FILE_SIZE_LIMITS, validateUpload } from '@hykocx/zen/storage'; + +// Get cookie name from environment or use default +const COOKIE_NAME = getSessionCookieName(); + +/** + * Get current user information + */ +export async function handleGetCurrentUser(request) { + // Get session token from cookies + const cookieStore = await cookies(); + const sessionToken = cookieStore.get(COOKIE_NAME)?.value; + + if (!sessionToken) { + return { + error: 'Unauthorized', + message: 'No session token provided' + }; + } + + // Validate session + const session = await validateSession(sessionToken); + + if (!session) { + return { + error: 'Unauthorized', + message: 'Invalid or expired session' + }; + } + + // Return user data (without sensitive information) + return { + user: { + id: session.user.id, + email: session.user.email, + name: session.user.name, + role: session.user.role, + image: session.user.image, + emailVerified: session.user.email_verified, + createdAt: session.user.created_at + } + }; +} + +/** + * Get user by ID (admin only) + */ +export async function handleGetUserById(request, userId) { + // Get session token from cookies + const cookieStore = await cookies(); + const sessionToken = cookieStore.get(COOKIE_NAME)?.value; + + if (!sessionToken) { + return { + error: 'Unauthorized', + message: 'No session token provided' + }; + } + + // Validate session + const session = await validateSession(sessionToken); + + if (!session) { + return { + error: 'Unauthorized', + message: 'Invalid or expired session' + }; + } + + // Check if user is admin + if (session.user.role !== 'admin') { + return { + error: 'Forbidden', + message: 'Admin access required' + }; + } + + // Get user from database + const result = await query( + 'SELECT id, email, name, role, image, email_verified, created_at FROM zen_auth_users WHERE id = $1', + [userId] + ); + + if (result.rows.length === 0) { + return { + error: 'Not Found', + message: 'User not found' + }; + } + + const response = { user: result.rows[0] }; + + // When clients module is active, include the client linked to this user (if any) + const modules = getModulesConfig(); + if (modules.clients) { + const clientResult = await query( + `SELECT id, client_number, company_name, first_name, last_name, email + FROM zen_clients WHERE user_id = $1 LIMIT 1`, + [userId] + ); + response.linkedClient = clientResult.rows[0] || null; + } + + return response; +} + +/** + * Update user by ID (admin only) + */ +export async function handleUpdateUserById(request, userId) { + const cookieStore = await cookies(); + const sessionToken = cookieStore.get(COOKIE_NAME)?.value; + + if (!sessionToken) { + return { success: false, error: 'Unauthorized', message: 'No session token provided' }; + } + + const session = await validateSession(sessionToken); + if (!session) { + return { success: false, error: 'Unauthorized', message: 'Invalid or expired session' }; + } + if (session.user.role !== 'admin') { + return { success: false, error: 'Forbidden', message: 'Admin access required' }; + } + + try { + const body = await request.json(); + const allowedFields = ['name', 'role', 'email_verified']; + const updateData = { updated_at: new Date() }; + + for (const field of allowedFields) { + if (body[field] !== undefined) { + if (field === 'email_verified') { + updateData[field] = Boolean(body[field]); + } else if (field === 'role') { + const role = String(body[field]).toLowerCase(); + if (['admin', 'user'].includes(role)) { + updateData[field] = role; + } + } else if (field === 'name' && body[field] != null) { + updateData[field] = String(body[field]).trim() || null; + } + } + } + + const updated = await updateById('zen_auth_users', userId, updateData); + if (!updated) { + return { success: false, error: 'Not Found', message: 'User not found' }; + } + + // When clients module is active, update client association (one user = one client) + const modules = getModulesConfig(); + if (modules.clients && body.client_id !== undefined) { + const clientId = body.client_id === null || body.client_id === '' ? null : parseInt(body.client_id, 10); + // Unlink all clients currently linked to this user + await query( + 'UPDATE zen_clients SET user_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE user_id = $1', + [userId] + ); + // Link the selected client to this user if provided + if (clientId != null && !Number.isNaN(clientId)) { + await query( + 'UPDATE zen_clients SET user_id = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2', + [userId, clientId] + ); + } + } + + const result = await query( + 'SELECT id, email, name, role, image, email_verified, created_at FROM zen_auth_users WHERE id = $1', + [userId] + ); + return { + success: true, + user: result.rows[0], + message: 'User updated successfully' + }; + } catch (error) { + console.error('Error updating user:', error); + return { + success: false, + error: 'Internal Server Error', + message: error.message || 'Failed to update user' + }; + } +} + +/** + * List all users (admin only) + */ +export async function handleListUsers(request) { + // Get session token from cookies + const cookieStore = await cookies(); + const sessionToken = cookieStore.get(COOKIE_NAME)?.value; + + if (!sessionToken) { + return { + error: 'Unauthorized', + message: 'No session token provided' + }; + } + + // Validate session + const session = await validateSession(sessionToken); + + if (!session) { + return { + error: 'Unauthorized', + message: 'Invalid or expired session' + }; + } + + // Check if user is admin + if (session.user.role !== 'admin') { + return { + error: 'Forbidden', + message: 'Admin access required' + }; + } + + // Get URL params for pagination and sorting + const url = new URL(request.url); + const page = parseInt(url.searchParams.get('page') || '1'); + const limit = parseInt(url.searchParams.get('limit') || '10'); + const offset = (page - 1) * limit; + + // Get sorting parameters + const sortBy = url.searchParams.get('sortBy') || 'created_at'; + const sortOrder = url.searchParams.get('sortOrder') || 'desc'; + + // Whitelist allowed sort columns to prevent SQL injection + const allowedSortColumns = ['id', 'email', 'name', 'role', 'email_verified', 'created_at']; + const sortColumn = allowedSortColumns.includes(sortBy) ? sortBy : 'created_at'; + + // Validate sort order + const order = sortOrder.toLowerCase() === 'asc' ? 'ASC' : 'DESC'; + + // Get users from database with dynamic sorting + const result = await query( + `SELECT id, email, name, role, image, email_verified, created_at FROM zen_auth_users ORDER BY ${sortColumn} ${order} LIMIT $1 OFFSET $2`, + [limit, offset] + ); + + // Get total count + const countResult = await query('SELECT COUNT(*) FROM zen_auth_users'); + const total = parseInt(countResult.rows[0].count); + + return { + users: result.rows, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + } + }; +} + +/** + * Update current user profile + */ +export async function handleUpdateProfile(request) { + // Get session token from cookies + const cookieStore = await cookies(); + const sessionToken = cookieStore.get(COOKIE_NAME)?.value; + + if (!sessionToken) { + return { + success: false, + error: 'Unauthorized', + message: 'No session token provided' + }; + } + + // Validate session + const session = await validateSession(sessionToken); + + if (!session) { + return { + success: false, + error: 'Unauthorized', + message: 'Invalid or expired session' + }; + } + + try { + // Get update data from request body + const body = await request.json(); + const { name } = body; + + // Validate input + if (!name || !name.trim()) { + return { + success: false, + error: 'Bad Request', + message: 'Name is required' + }; + } + + // Prepare update data + const updateData = { + name: name.trim() + }; + + // Update user profile + const updatedUser = await updateUser(session.user.id, updateData); + + // Return updated user data (without sensitive information) + return { + success: true, + user: { + id: updatedUser.id, + email: updatedUser.email, + name: updatedUser.name, + role: updatedUser.role, + image: updatedUser.image, + email_verified: updatedUser.email_verified, + created_at: updatedUser.created_at + }, + message: 'Profile updated successfully' + }; + } catch (error) { + console.error('Error updating profile:', error); + return { + success: false, + error: 'Internal Server Error', + message: error.message || 'Failed to update profile' + }; + } +} + +/** + * Upload profile picture + */ +export async function handleUploadProfilePicture(request) { + // Get session token from cookies + const cookieStore = await cookies(); + const sessionToken = cookieStore.get(COOKIE_NAME)?.value; + + if (!sessionToken) { + return { + success: false, + error: 'Unauthorized', + message: 'No session token provided' + }; + } + + // Validate session + const session = await validateSession(sessionToken); + + if (!session) { + return { + success: false, + error: 'Unauthorized', + message: 'Invalid or expired session' + }; + } + + try { + // Get form data from request + const formData = await request.formData(); + const file = formData.get('file'); + + if (!file) { + return { + success: false, + error: 'Bad Request', + message: 'No file provided' + }; + } + + // Validate file + const validation = validateUpload({ + filename: file.name, + size: file.size, + allowedTypes: FILE_TYPE_PRESETS.IMAGES, + maxSize: FILE_SIZE_LIMITS.AVATAR + }); + + if (!validation.valid) { + return { + success: false, + error: 'Bad Request', + message: validation.errors.join(', ') + }; + } + + // Get current user to check for existing profile picture + const currentUser = await query( + 'SELECT image FROM zen_auth_users WHERE id = $1', + [session.user.id] + ); + + let oldImageKey = null; + if (currentUser.rows.length > 0 && currentUser.rows[0].image) { + // The image field now contains the storage key directly + oldImageKey = currentUser.rows[0].image; + } + + // Generate unique filename + const uniqueFilename = generateUniqueFilename(file.name, 'avatar'); + + // Generate storage path + const key = generateUserFilePath(session.user.id, 'profile', uniqueFilename); + + // Convert file to buffer + const buffer = Buffer.from(await file.arrayBuffer()); + + // Upload to storage + const uploadResult = await uploadImage({ + key, + body: buffer, + contentType: file.type, + metadata: { + userId: session.user.id, + originalName: file.name + } + }); + + if (!uploadResult.success) { + return { + success: false, + error: 'Upload Failed', + message: uploadResult.error || 'Failed to upload image' + }; + } + + // Update user profile with storage key (not URL) + const updatedUser = await updateUser(session.user.id, { + image: key + }); + + // Delete old image if it exists (after successful upload) + if (oldImageKey) { + try { + await deleteFile(oldImageKey); + console.log(`[ZEN] Deleted old profile picture: ${oldImageKey}`); + } catch (deleteError) { + // Log error but don't fail the upload + console.error('[ZEN] Failed to delete old profile picture:', deleteError); + } + } + + // Return updated user data + return { + success: true, + user: { + id: updatedUser.id, + email: updatedUser.email, + name: updatedUser.name, + role: updatedUser.role, + image: updatedUser.image, + email_verified: updatedUser.email_verified, + created_at: updatedUser.created_at + }, + message: 'Profile picture uploaded successfully' + }; + } catch (error) { + console.error('Error uploading profile picture:', error); + return { + success: false, + error: 'Internal Server Error', + message: error.message || 'Failed to upload profile picture' + }; + } +} + +/** + * Delete profile picture + */ +export async function handleDeleteProfilePicture(request) { + // Get session token from cookies + const cookieStore = await cookies(); + const sessionToken = cookieStore.get(COOKIE_NAME)?.value; + + if (!sessionToken) { + return { + success: false, + error: 'Unauthorized', + message: 'No session token provided' + }; + } + + // Validate session + const session = await validateSession(sessionToken); + + if (!session) { + return { + success: false, + error: 'Unauthorized', + message: 'Invalid or expired session' + }; + } + + try { + // Get current user to check for existing profile picture + const currentUser = await query( + 'SELECT image FROM zen_auth_users WHERE id = $1', + [session.user.id] + ); + + if (currentUser.rows.length === 0) { + return { + success: false, + error: 'Not Found', + message: 'User not found' + }; + } + + const imageKey = currentUser.rows[0].image; + if (!imageKey) { + return { + success: false, + error: 'Bad Request', + message: 'No profile picture to delete' + }; + } + + // Update user profile to remove image URL + const updatedUser = await updateUser(session.user.id, { + image: null + }); + + // Delete image from storage + if (imageKey) { + try { + await deleteFile(imageKey); + console.log(`[ZEN] Deleted profile picture: ${imageKey}`); + } catch (deleteError) { + // Log error but don't fail the update + console.error('[ZEN] Failed to delete profile picture from storage:', deleteError); + } + } + + // Return updated user data + return { + success: true, + user: { + id: updatedUser.id, + email: updatedUser.email, + name: updatedUser.name, + role: updatedUser.role, + image: updatedUser.image, + email_verified: updatedUser.email_verified, + created_at: updatedUser.created_at + }, + message: 'Profile picture deleted successfully' + }; + } catch (error) { + console.error('Error deleting profile picture:', error); + return { + success: false, + error: 'Internal Server Error', + message: error.message || 'Failed to delete profile picture' + }; + } +} + diff --git a/src/core/api/handlers/version.js b/src/core/api/handlers/version.js new file mode 100644 index 0000000..48c1ca1 --- /dev/null +++ b/src/core/api/handlers/version.js @@ -0,0 +1,16 @@ +/** + * Version Handler + * Returns version information about the ZEN API + */ + +import { getAppName } from '../../../shared/lib/appConfig.js'; + +export async function handleVersion() { + return { + name: 'ZEN API', + appName: getAppName(), + version: '0.1.0', + apiVersion: '1.0', + description: 'ZEN API - Complete modular web platform' + }; +} diff --git a/src/core/api/index.js b/src/core/api/index.js new file mode 100644 index 0000000..67ce3c1 --- /dev/null +++ b/src/core/api/index.js @@ -0,0 +1,21 @@ +/** + * Zen API Module + * + * This module exports API utilities for custom handlers + * For route setup, import from '@hykocx/zen/zen/api' + */ + +// Export router utilities (for custom handlers) +export { routeRequest, getStatusCode, requireAuth, requireAdmin } from './router.js'; + +// Export individual handlers (for custom usage) +export { handleHealth } from './handlers/health.js'; +export { handleVersion } from './handlers/version.js'; +export { + handleGetCurrentUser, + handleGetUserById, + handleListUsers +} from './handlers/users.js'; + +// Module API handlers are now self-contained in their respective modules +// e.g., invoice handlers are in @hykocx/zen/modules/invoice/api diff --git a/src/core/api/nx-route.js b/src/core/api/nx-route.js new file mode 100644 index 0000000..559b404 --- /dev/null +++ b/src/core/api/nx-route.js @@ -0,0 +1,166 @@ +/** + * ZEN API Route Handler + * + * This is the main catch-all route handler for the ZEN API under /zen/api/. + * It should be used in a Next.js App Router route at: app/zen/api/[...path]/route.js + */ + +import { NextResponse } from 'next/server'; +import { routeRequest, getStatusCode } from './router.js'; + +/** + * Handle GET requests + */ +export async function GET(request, { params }) { + try { + const resolvedParams = await params; + const path = resolvedParams.path || []; + const response = await routeRequest(request, path); + + // Check if this is a file response (from storage endpoint) + if (response.success && response.file) { + const headers = { + 'Content-Type': response.file.contentType || 'application/octet-stream', + 'Content-Length': response.file.contentLength?.toString() || '', + 'Cache-Control': 'private, max-age=3600', + 'Last-Modified': response.file.lastModified?.toUTCString() || new Date().toUTCString(), + }; + if (response.file.filename) { + const encoded = encodeURIComponent(response.file.filename); + headers['Content-Disposition'] = `attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`; + } + return new NextResponse(response.file.body, { status: 200, headers }); + } + + // Regular JSON response + const statusCode = getStatusCode(response); + return NextResponse.json(response, { + status: statusCode, + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('API Error:', error); + return NextResponse.json( + { + error: 'Internal Server Error', + message: error.message + }, + { status: 500 } + ); + } +} + +/** + * Handle POST requests + */ +export async function POST(request, { params }) { + try { + const resolvedParams = await params; + const path = resolvedParams.path || []; + const response = await routeRequest(request, path); + const statusCode = getStatusCode(response); + + return NextResponse.json(response, { + status: statusCode, + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('API Error:', error); + return NextResponse.json( + { + error: 'Internal Server Error', + message: error.message + }, + { status: 500 } + ); + } +} + +/** + * Handle PUT requests + */ +export async function PUT(request, { params }) { + try { + const resolvedParams = await params; + const path = resolvedParams.path || []; + const response = await routeRequest(request, path); + const statusCode = getStatusCode(response); + + return NextResponse.json(response, { + status: statusCode, + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('API Error:', error); + return NextResponse.json( + { + error: 'Internal Server Error', + message: error.message + }, + { status: 500 } + ); + } +} + +/** + * Handle DELETE requests + */ +export async function DELETE(request, { params }) { + try { + const resolvedParams = await params; + const path = resolvedParams.path || []; + const response = await routeRequest(request, path); + const statusCode = getStatusCode(response); + + return NextResponse.json(response, { + status: statusCode, + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('API Error:', error); + return NextResponse.json( + { + error: 'Internal Server Error', + message: error.message + }, + { status: 500 } + ); + } +} + +/** + * Handle PATCH requests + */ +export async function PATCH(request, { params }) { + try { + const resolvedParams = await params; + const path = resolvedParams.path || []; + const response = await routeRequest(request, path); + const statusCode = getStatusCode(response); + + return NextResponse.json(response, { + status: statusCode, + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('API Error:', error); + return NextResponse.json( + { + error: 'Internal Server Error', + message: error.message + }, + { status: 500 } + ); + } +} + diff --git a/src/core/api/router.js b/src/core/api/router.js new file mode 100644 index 0000000..46342f0 --- /dev/null +++ b/src/core/api/router.js @@ -0,0 +1,320 @@ +/** + * API Router + * Routes incoming requests to appropriate handlers + * + * This router supports both: + * - Core routes (health, version, users, storage) + * - Module routes (imported directly from module api.js files) + */ + +import { validateSession } from '../../features/auth/lib/session.js'; +import { cookies } from 'next/headers'; +import { getSessionCookieName } from '../../shared/lib/appConfig.js'; +import { getAllApiRoutes } from '../modules/index.js'; +import { checkRateLimit, getIpFromRequest, formatRetryAfter } from '../../features/auth/lib/rateLimit.js'; + +// Core handlers +import { handleHealth } from './handlers/health.js'; +import { handleVersion } from './handlers/version.js'; +import { + handleGetCurrentUser, + handleGetUserById, + handleListUsers, + handleUpdateProfile, + handleUpdateUserById, + handleUploadProfilePicture, + handleDeleteProfilePicture +} from './handlers/users.js'; +import { handleGetFile } from './handlers/storage.js'; + +// Get cookie name from environment or use default +const COOKIE_NAME = getSessionCookieName(); + +/** + * Get all module routes from the dynamic module registry + * @returns {Array} Array of route definitions + */ +function getModuleRoutes() { + // Use the dynamic module registry to get all routes + return getAllApiRoutes(); +} + +/** + * Check if user is authenticated + * @param {Request} request - The request object + * @returns {Promise} Session object if authenticated + * @throws {Error} If not authenticated + */ +async function requireAuth(request) { + const cookieStore = await cookies(); + const sessionToken = cookieStore.get(COOKIE_NAME)?.value; + + if (!sessionToken) { + throw new Error('Unauthorized'); + } + + const session = await validateSession(sessionToken); + + if (!session || !session.user) { + throw new Error('Unauthorized'); + } + + return session; +} + +/** + * Check if user is authenticated and is admin + * @param {Request} request - The request object + * @returns {Promise} Session object if authenticated and admin + * @throws {Error} If not authenticated or not admin + */ +async function requireAdmin(request) { + const session = await requireAuth(request); + + if (session.user.role !== 'admin') { + throw new Error('Admin access required'); + } + + return session; +} + +/** + * Route an API request to the appropriate handler + * @param {Request} request - The incoming request + * @param {string[]} path - The path segments after /zen/api/ + * @returns {Promise} - The response data + */ +export async function routeRequest(request, path) { + const method = request.method; + + // Global IP-based rate limit for all API calls (health/version are exempt) + const isExempt = (path[0] === 'health' || path[0] === 'version') && method === 'GET'; + if (!isExempt) { + const ip = getIpFromRequest(request); + const rl = checkRateLimit(ip, 'api'); + if (!rl.allowed) { + return { + error: 'Too Many Requests', + message: `Trop de requêtes. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.` + }; + } + } + + // Try core routes first + const coreResult = await routeCoreRequest(request, path, method); + if (coreResult !== null) { + return coreResult; + } + + // Try module routes (dynamically discovered) + const moduleResult = await routeModuleRequest(request, path, method); + if (moduleResult !== null) { + return moduleResult; + } + + // No matching route + return { + error: 'Not Found', + message: `No handler found for ${method} ${path.join('/')}`, + path: path + }; +} + +/** + * Route core (non-module) requests + * @param {Request} request - The incoming request + * @param {string[]} path - The path segments + * @param {string} method - HTTP method + * @returns {Promise} Response or null if no match + */ +async function routeCoreRequest(request, path, method) { + // Health check endpoint + if (path[0] === 'health' && method === 'GET') { + return await handleHealth(); + } + + // Version endpoint + if (path[0] === 'version' && method === 'GET') { + return await handleVersion(); + } + + // Storage endpoint - serve files securely + if (path[0] === 'storage' && method === 'GET') { + const fileKey = path.slice(1).join('/'); + if (!fileKey) { + return { + error: 'Bad Request', + message: 'File path is required' + }; + } + return await handleGetFile(request, fileKey); + } + + // User endpoints + if (path[0] === 'users') { + // GET /zen/api/users - List all users (admin only) + if (path.length === 1 && method === 'GET') { + return await handleListUsers(request); + } + + // GET /zen/api/users/me - Get current user + if (path[1] === 'me' && method === 'GET') { + return await handleGetCurrentUser(request); + } + + // PUT /zen/api/users/profile - Update current user profile + if (path[1] === 'profile' && method === 'PUT') { + return await handleUpdateProfile(request); + } + + // POST /zen/api/users/profile/picture - Upload profile picture + if (path[1] === 'profile' && path[2] === 'picture' && method === 'POST') { + return await handleUploadProfilePicture(request); + } + + // DELETE /zen/api/users/profile/picture - Delete profile picture + if (path[1] === 'profile' && path[2] === 'picture' && method === 'DELETE') { + return await handleDeleteProfilePicture(request); + } + + // GET /zen/api/users/:id - Get user by ID (admin only) + if (path[1] && path[1] !== 'me' && path[1] !== 'profile' && method === 'GET') { + return await handleGetUserById(request, path[1]); + } + + // PUT /zen/api/users/:id - Update user by ID (admin only) + if (path[1] && path[1] !== 'me' && path[1] !== 'profile' && method === 'PUT') { + return await handleUpdateUserById(request, path[1]); + } + } + + return null; +} + +/** + * Route module requests + * @param {Request} request - The incoming request + * @param {string[]} path - The path segments + * @param {string} method - HTTP method + * @returns {Promise} Response or null if no match + */ +async function routeModuleRequest(request, path, method) { + // Get routes from enabled modules + const routes = getModuleRoutes(); + + // Convert path array to path string + const pathString = '/' + path.join('/'); + + // Find matching route + for (const route of routes) { + if (matchRoute(route.path, pathString) && route.method === method) { + // Check authentication + try { + if (route.auth === 'admin') { + await requireAdmin(request); + } else if (route.auth === 'user' || route.auth === 'auth') { + await requireAuth(request); + } + // 'public' or undefined means no auth required + + // Call the handler + if (typeof route.handler === 'function') { + // Extract path parameters + const params = extractPathParams(route.path, pathString); + return await route.handler(request, params); + } + } catch (authError) { + return { + success: false, + error: authError.message + }; + } + } + } + + return null; +} + +/** + * Match a route pattern against a path + * Supports path parameters like :id + * @param {string} pattern - Route pattern (e.g., '/admin/invoices/:id') + * @param {string} path - Actual path (e.g., '/admin/invoices/123') + * @returns {boolean} True if matches + */ +function matchRoute(pattern, path) { + const patternParts = pattern.split('/').filter(Boolean); + const pathParts = path.split('/').filter(Boolean); + + if (patternParts.length !== pathParts.length) { + return false; + } + + for (let i = 0; i < patternParts.length; i++) { + const patternPart = patternParts[i]; + const pathPart = pathParts[i]; + + // Skip parameter parts (they match anything) + if (patternPart.startsWith(':')) { + continue; + } + + if (patternPart !== pathPart) { + return false; + } + } + + return true; +} + +/** + * Extract path parameters from a path + * @param {string} pattern - Route pattern (e.g., '/admin/invoices/:id') + * @param {string} path - Actual path (e.g., '/admin/invoices/123') + * @returns {Object} Path parameters + */ +function extractPathParams(pattern, path) { + const params = {}; + const patternParts = pattern.split('/').filter(Boolean); + const pathParts = path.split('/').filter(Boolean); + + for (let i = 0; i < patternParts.length; i++) { + const patternPart = patternParts[i]; + + if (patternPart.startsWith(':')) { + const paramName = patternPart.slice(1); + params[paramName] = pathParts[i]; + } + } + + return params; +} + +/** + * Get the HTTP status code based on the response + * @param {Object} response - The response object + * @returns {number} - HTTP status code + */ +export function getStatusCode(response) { + if (response.error) { + switch (response.error) { + case 'Unauthorized': + return 401; + case 'Forbidden': + case 'Admin access required': + return 403; + case 'Not Found': + return 404; + case 'Bad Request': + return 400; + case 'Too Many Requests': + return 429; + default: + return 500; + } + } + return 200; +} + +// Export auth helpers for use in module handlers +export { requireAuth, requireAdmin }; diff --git a/src/core/cron/index.js b/src/core/cron/index.js new file mode 100644 index 0000000..77af15b --- /dev/null +++ b/src/core/cron/index.js @@ -0,0 +1,183 @@ +/** + * Cron Utility + * Wrapper around node-cron for scheduling tasks + * + * Usage in modules: + * import { schedule, validate } from '@hykocx/zen/cron'; + */ + +import cron from 'node-cron'; + +// Store for all scheduled cron jobs +const CRON_JOBS_KEY = Symbol.for('__ZEN_CRON_JOBS__'); + +/** + * Initialize cron jobs storage + */ +function getJobsStorage() { + if (!globalThis[CRON_JOBS_KEY]) { + globalThis[CRON_JOBS_KEY] = new Map(); + } + return globalThis[CRON_JOBS_KEY]; +} + +/** + * Schedule a cron job + * @param {string} name - Unique name for the job + * @param {string} schedule - Cron schedule expression + * @param {Function} handler - Handler function to execute + * @param {Object} options - Options + * @param {string} options.timezone - Timezone (default: from env or America/Toronto) + * @param {boolean} options.runOnInit - Run immediately on schedule (default: false) + * @returns {Object} Cron job instance + * + * @example + * schedule('my-task', '0 9 * * *', async () => { + * console.log('Running every day at 9 AM'); + * }); + * + * @example + * schedule('reminder', ''\''*\/5 5-17 * * *'\'', async () => { + * console.log('Every 5 minutes between 5 AM and 5 PM'); + * }, { timezone: 'America/New_York' }); + */ +export function schedule(name, cronSchedule, handler, options = {}) { + const jobs = getJobsStorage(); + + // Stop existing job with same name + if (jobs.has(name)) { + jobs.get(name).stop(); + console.log(`[Cron] Stopped existing job: ${name}`); + } + + const timezone = options.timezone || process.env.ZEN_TIMEZONE || 'America/Toronto'; + + const job = cron.schedule(cronSchedule, async () => { + console.log(`[Cron: ${name}] Running at:`, new Date().toISOString()); + + try { + await handler(); + console.log(`[Cron: ${name}] Completed`); + } catch (error) { + console.error(`[Cron: ${name}] Error:`, error); + } + }, { + scheduled: true, + timezone, + runOnInit: options.runOnInit || false + }); + + jobs.set(name, job); + console.log(`[Cron] Scheduled job: ${name} (${cronSchedule})`); + + return job; +} + +/** + * Stop a scheduled cron job + * @param {string} name - Job name + * @returns {boolean} True if job was stopped + */ +export function stop(name) { + const jobs = getJobsStorage(); + + if (jobs.has(name)) { + jobs.get(name).stop(); + jobs.delete(name); + console.log(`[Cron] Stopped job: ${name}`); + return true; + } + + return false; +} + +/** + * Stop all cron jobs + */ +export function stopAll() { + const jobs = getJobsStorage(); + + for (const [name, job] of jobs.entries()) { + job.stop(); + console.log(`[Cron] Stopped job: ${name}`); + } + + jobs.clear(); + console.log('[Cron] All jobs stopped'); +} + +/** + * Get status of all cron jobs + * @returns {Object} Status of all jobs + */ +export function getStatus() { + const jobs = getJobsStorage(); + const status = {}; + + for (const [name] of jobs.entries()) { + status[name] = { running: true }; + } + + return status; +} + +/** + * Check if a cron job is running + * @param {string} name - Job name + * @returns {boolean} + */ +export function isRunning(name) { + const jobs = getJobsStorage(); + return jobs.has(name); +} + +/** + * Validate a cron expression + * @param {string} expression - Cron expression to validate + * @returns {boolean} True if valid + */ +export function validate(expression) { + return cron.validate(expression); +} + +/** + * Get list of all scheduled job names + * @returns {string[]} Array of job names + */ +export function getJobs() { + const jobs = getJobsStorage(); + return Array.from(jobs.keys()); +} + +/** + * Manually trigger a job by name + * @param {string} name - Job name + * @returns {Promise} + */ +export async function trigger(name) { + const jobs = getJobsStorage(); + + if (!jobs.has(name)) { + throw new Error(`Cron job '${name}' not found`); + } + + console.log(`[Cron] Manual trigger for: ${name}`); + // Note: node-cron doesn't expose the handler directly, + // so modules should keep their handler function accessible +} + +// Re-export the raw cron module for advanced usage +export { cron }; + +// Default export for convenience +export default { + schedule, + stop, + stopAll, + getStatus, + isRunning, + validate, + getJobs, + trigger, + cron +}; diff --git a/src/core/database/cli.js b/src/core/database/cli.js new file mode 100644 index 0000000..f8278b5 --- /dev/null +++ b/src/core/database/cli.js @@ -0,0 +1,127 @@ +#!/usr/bin/env node + +/** + * Zen Database CLI + * Command-line tool for database management + */ + +// Load environment variables from the project's .env file +import dotenv from 'dotenv'; +import { resolve } from 'node:path'; + +// Load .env from the current working directory (user's project) +dotenv.config({ path: resolve(process.cwd(), '.env') }); +dotenv.config({ path: resolve(process.cwd(), '.env.local') }); + +// The CLI always runs locally, so default to development to use ZEN_DATABASE_URL_DEV if set +process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + +import { initDatabase, dropAuthTables, testConnection, closePool } from './index.js'; +import readline from 'readline'; + +async function runCLI() { + const command = process.argv[2]; + + if (!command) { + console.log(` +Zen Database CLI + +Usage: + npx zen-db + +Commands: + init Initialize database (create all required tables) + test Test database connection + drop Drop all authentication tables (DANGER!) + help Show this help message + +Example: + npx zen-db init + `); + process.exit(0); + } + + try { + switch (command) { + case 'init': + console.log('🔧 Initializing database...\n'); + const result = await initDatabase(); + console.log(`\n✅ Success! Created ${result.created.length} tables, skipped ${result.skipped.length} existing tables.`); + break; + + case 'test': + console.log('🔌 Testing database connection...\n'); + const isConnected = await testConnection(); + if (isConnected) { + console.log('✅ Database connection successful!'); + } else { + console.log('❌ Database connection failed!'); + process.exit(1); + } + break; + + case 'drop': + console.log('⚠️ WARNING: This will delete all authentication tables!\n'); + console.log('Type "yes" to confirm or Ctrl+C to cancel...'); + + // Simple confirmation (in production, you'd use a proper readline) + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + rl.question('Confirm (yes/no): ', async (answer) => { + if (answer.toLowerCase() === 'yes') { + await dropAuthTables(); + console.log('✅ Tables dropped successfully.'); + } else { + console.log('❌ Operation cancelled.'); + } + rl.close(); + process.exit(0); + }); + return; // Don't close the process yet + + case 'help': + console.log(` +Zen Database CLI + +Commands: + init Initialize database (create all required tables) + test Test database connection + drop Drop all authentication tables (DANGER!) + help Show this help message + +Usage: + npx zen-db + `); + break; + + default: + console.log(`❌ Unknown command: ${command}`); + console.log('Run "npx zen-db help" for usage information.'); + process.exit(1); + } + + // Close the database connection pool + await closePool(); + process.exit(0); + + } catch (error) { + console.error('❌ Error:', error.message); + process.exit(1); + } +} + +// Run CLI if called directly +import { fileURLToPath } from 'url'; +import { realpathSync } from 'node:fs'; +const __filename = realpathSync(fileURLToPath(import.meta.url)); +const isMainModule = process.argv[1] && realpathSync(process.argv[1]) === __filename; + +if (isMainModule) { + runCLI(); +} + +export { runCLI }; + diff --git a/src/core/database/crud.js b/src/core/database/crud.js new file mode 100644 index 0000000..94b8108 --- /dev/null +++ b/src/core/database/crud.js @@ -0,0 +1,223 @@ +/** + * CRUD Helper Functions + * Provides convenient methods for Create, Read, Update, Delete operations + */ + +import { query, queryOne, queryAll } from './db.js'; + +/** + * Insert a new record into a table + * @param {string} tableName - Name of the table + * @param {Object} data - Object with column names as keys and values to insert + * @returns {Promise} Inserted record with all fields + */ +async function create(tableName, data) { + const columns = Object.keys(data); + const values = Object.values(data); + const placeholders = values.map((_, index) => `$${index + 1}`).join(', '); + + const sql = ` + INSERT INTO ${tableName} (${columns.join(', ')}) + VALUES (${placeholders}) + RETURNING * + `; + + const result = await query(sql, values); + return result.rows[0]; +} + +/** + * Find a single record by ID + * @param {string} tableName - Name of the table + * @param {number|string} id - ID of the record + * @param {string} idColumn - Name of the ID column (default: 'id') + * @returns {Promise} Found record or null + */ +async function findById(tableName, id, idColumn = 'id') { + const sql = `SELECT * FROM ${tableName} WHERE ${idColumn} = $1`; + return await queryOne(sql, [id]); +} + +/** + * Find records matching conditions + * @param {string} tableName - Name of the table + * @param {Object} conditions - Object with column names as keys and values to match + * @param {Object} options - Query options (limit, offset, orderBy) + * @returns {Promise} Array of matching records + */ +async function find(tableName, conditions = {}, options = {}) { + const { limit, offset, orderBy } = options; + + let sql = `SELECT * FROM ${tableName}`; + const values = []; + + // Build WHERE clause + if (Object.keys(conditions).length > 0) { + const whereConditions = Object.keys(conditions).map((key, index) => { + values.push(conditions[key]); + return `${key} = $${index + 1}`; + }); + sql += ` WHERE ${whereConditions.join(' AND ')}`; + } + + // Add ORDER BY + if (orderBy) { + sql += ` ORDER BY ${orderBy}`; + } + + // Add LIMIT + if (limit) { + sql += ` LIMIT ${parseInt(limit)}`; + } + + // Add OFFSET + if (offset) { + sql += ` OFFSET ${parseInt(offset)}`; + } + + return await queryAll(sql, values); +} + +/** + * Find a single record matching conditions + * @param {string} tableName - Name of the table + * @param {Object} conditions - Object with column names as keys and values to match + * @returns {Promise} Found record or null + */ +async function findOne(tableName, conditions) { + const results = await find(tableName, conditions, { limit: 1 }); + return results.length > 0 ? results[0] : null; +} + +/** + * Update a record by ID + * @param {string} tableName - Name of the table + * @param {number|string} id - ID of the record + * @param {Object} data - Object with column names as keys and new values + * @param {string} idColumn - Name of the ID column (default: 'id') + * @returns {Promise} Updated record or null if not found + */ +async function updateById(tableName, id, data, idColumn = 'id') { + const columns = Object.keys(data); + const values = Object.values(data); + + const setClause = columns.map((col, index) => `${col} = $${index + 1}`).join(', '); + + const sql = ` + UPDATE ${tableName} + SET ${setClause} + WHERE ${idColumn} = $${values.length + 1} + RETURNING * + `; + + const result = await query(sql, [...values, id]); + return result.rows.length > 0 ? result.rows[0] : null; +} + +/** + * Update records matching conditions + * @param {string} tableName - Name of the table + * @param {Object} conditions - Object with column names as keys and values to match + * @param {Object} data - Object with column names as keys and new values + * @returns {Promise} Array of updated records + */ +async function update(tableName, conditions, data) { + const dataColumns = Object.keys(data); + const dataValues = Object.values(data); + + const setClause = dataColumns.map((col, index) => `${col} = $${index + 1}`).join(', '); + + let paramIndex = dataValues.length + 1; + const whereConditions = Object.keys(conditions).map((key) => { + dataValues.push(conditions[key]); + return `${key} = $${paramIndex++}`; + }); + + const sql = ` + UPDATE ${tableName} + SET ${setClause} + WHERE ${whereConditions.join(' AND ')} + RETURNING * + `; + + const result = await query(sql, dataValues); + return result.rows; +} + +/** + * Delete a record by ID + * @param {string} tableName - Name of the table + * @param {number|string} id - ID of the record + * @param {string} idColumn - Name of the ID column (default: 'id') + * @returns {Promise} True if record was deleted, false otherwise + */ +async function deleteById(tableName, id, idColumn = 'id') { + const sql = `DELETE FROM ${tableName} WHERE ${idColumn} = $1 RETURNING *`; + const result = await query(sql, [id]); + return result.rows.length > 0; +} + +/** + * Delete records matching conditions + * @param {string} tableName - Name of the table + * @param {Object} conditions - Object with column names as keys and values to match + * @returns {Promise} Number of deleted records + */ +async function deleteWhere(tableName, conditions) { + const values = []; + const whereConditions = Object.keys(conditions).map((key, index) => { + values.push(conditions[key]); + return `${key} = $${index + 1}`; + }); + + const sql = `DELETE FROM ${tableName} WHERE ${whereConditions.join(' AND ')} RETURNING *`; + const result = await query(sql, values); + return result.rowCount; +} + +/** + * Count records in a table + * @param {string} tableName - Name of the table + * @param {Object} conditions - Object with column names as keys and values to match (optional) + * @returns {Promise} Number of records + */ +async function count(tableName, conditions = {}) { + let sql = `SELECT COUNT(*) as count FROM ${tableName}`; + const values = []; + + if (Object.keys(conditions).length > 0) { + const whereConditions = Object.keys(conditions).map((key, index) => { + values.push(conditions[key]); + return `${key} = $${index + 1}`; + }); + sql += ` WHERE ${whereConditions.join(' AND ')}`; + } + + const result = await queryOne(sql, values); + return parseInt(result.count); +} + +/** + * Check if a record exists + * @param {string} tableName - Name of the table + * @param {Object} conditions - Object with column names as keys and values to match + * @returns {Promise} True if record exists, false otherwise + */ +async function exists(tableName, conditions) { + const recordCount = await count(tableName, conditions); + return recordCount > 0; +} + +export { + create, + findById, + find, + findOne, + updateById, + update, + deleteById, + deleteWhere, + count, + exists +}; + diff --git a/src/core/database/db.js b/src/core/database/db.js new file mode 100644 index 0000000..b0660b9 --- /dev/null +++ b/src/core/database/db.js @@ -0,0 +1,148 @@ +/** + * Database Connection and Query Utilities + * Provides PostgreSQL database connection and query execution functions + */ + +import pkg from 'pg'; +const { Pool } = pkg; + +let pool = null; + +function resolveDatabaseUrl() { + const isDev = process.env.NODE_ENV === 'development'; + if (isDev && process.env.ZEN_DATABASE_URL_DEV) { + return process.env.ZEN_DATABASE_URL_DEV; + } + return process.env.ZEN_DATABASE_URL; +} + +/** + * Get or create a database connection pool + * @returns {Pool} PostgreSQL connection pool + */ +function getPool() { + if (!pool) { + const databaseUrl = resolveDatabaseUrl(); + + if (!databaseUrl) { + throw new Error( + process.env.NODE_ENV === 'development' + ? 'ZEN_DATABASE_URL or ZEN_DATABASE_URL_DEV must be defined in environment variables' + : 'ZEN_DATABASE_URL is not defined in environment variables' + ); + } + + pool = new Pool({ + connectionString: databaseUrl, + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, + max: 20, // Maximum number of clients in the pool + idleTimeoutMillis: 30000, // Close idle clients after 30 seconds + connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established + }); + + // Handle pool errors + pool.on('error', (err) => { + console.error('Unexpected error on idle client', err); + }); + } + + return pool; +} + +/** + * Execute a SQL query + * @param {string} sql - SQL query string + * @param {Array} params - Query parameters (optional) + * @returns {Promise} Query result + */ +async function query(sql, params = []) { + const client = getPool(); + + try { + const result = await client.query(sql, params); + return result; + } catch (error) { + console.error('Database query error:', error); + throw error; + } +} + +/** + * Execute a query and return the first row + * @param {string} sql - SQL query string + * @param {Array} params - Query parameters (optional) + * @returns {Promise} First row or null if no results + */ +async function queryOne(sql, params = []) { + const result = await query(sql, params); + return result.rows.length > 0 ? result.rows[0] : null; +} + +/** + * Execute a query and return all rows + * @param {string} sql - SQL query string + * @param {Array} params - Query parameters (optional) + * @returns {Promise} Array of rows + */ +async function queryAll(sql, params = []) { + const result = await query(sql, params); + return result.rows; +} + +/** + * Execute multiple queries in a transaction + * @param {Function} callback - Async function that receives a client and executes queries + * @returns {Promise} Result of the callback function + */ +async function transaction(callback) { + const client = await getPool().connect(); + + try { + await client.query('BEGIN'); + const result = await callback(client); + await client.query('COMMIT'); + return result; + } catch (error) { + await client.query('ROLLBACK'); + console.error('Transaction error:', error); + throw error; + } finally { + client.release(); + } +} + +/** + * Close the database connection pool + * @returns {Promise} + */ +async function closePool() { + if (pool) { + await pool.end(); + pool = null; + } +} + +/** + * Test database connection + * @returns {Promise} True if connection successful + */ +async function testConnection() { + try { + await query('SELECT NOW()'); + return true; + } catch (error) { + console.error('Database connection test failed:', error); + return false; + } +} + +export { + query, + queryOne, + queryAll, + transaction, + getPool, + closePool, + testConnection +}; + diff --git a/src/core/database/index.js b/src/core/database/index.js new file mode 100644 index 0000000..73b7cd0 --- /dev/null +++ b/src/core/database/index.js @@ -0,0 +1,38 @@ +/** + * Zen Database Module + * Complete database utilities for PostgreSQL + */ + +// Core database functions +export { + query, + queryOne, + queryAll, + transaction, + getPool, + closePool, + testConnection +} from './db.js'; + +// CRUD helper functions +export { + create, + findById, + find, + findOne, + updateById, + update, + deleteById, + deleteWhere, + count, + exists +} from './crud.js'; + +// Database initialization +export { + initDatabase, + createAuthTables, + tableExists, + dropAuthTables +} from './init.js'; + diff --git a/src/core/database/init.js b/src/core/database/init.js new file mode 100644 index 0000000..d661164 --- /dev/null +++ b/src/core/database/init.js @@ -0,0 +1,187 @@ +/** + * Database Initialization + * Creates required tables if they don't exist + */ + +import { query } from './db.js'; + +/** + * Check if a table exists in the database + * @param {string} tableName - Name of the table to check + * @returns {Promise} True if table exists, false otherwise + */ +async function tableExists(tableName) { + const result = await query( + `SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = $1 + )`, + [tableName] + ); + return result.rows[0].exists; +} + +/** + * Create authentication tables + * @returns {Promise} + */ +async function createAuthTables() { + const tables = [ + { + name: 'zen_auth_users', + sql: ` + CREATE TABLE zen_auth_users ( + id text NOT NULL PRIMARY KEY, + name text NOT NULL, + email text NOT NULL UNIQUE, + email_verified boolean NOT NULL DEFAULT false, + image text, + created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + role text DEFAULT 'user' + ) + ` + }, + { + name: 'zen_auth_sessions', + sql: ` + CREATE TABLE zen_auth_sessions ( + id text NOT NULL PRIMARY KEY, + expires_at timestamptz NOT NULL, + token text NOT NULL UNIQUE, + created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamptz NOT NULL, + ip_address text, + user_agent text, + user_id text NOT NULL REFERENCES zen_auth_users (id) ON DELETE CASCADE + ) + ` + }, + { + name: 'zen_auth_accounts', + sql: ` + CREATE TABLE zen_auth_accounts ( + id text NOT NULL PRIMARY KEY, + account_id text NOT NULL, + provider_id text NOT NULL, + user_id text NOT NULL REFERENCES zen_auth_users (id) ON DELETE CASCADE, + access_token text, + refresh_token text, + id_token text, + access_token_expires_at timestamptz, + refresh_token_expires_at timestamptz, + scope text, + password text, + created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamptz NOT NULL + ) + ` + }, + { + name: 'zen_auth_verifications', + sql: ` + CREATE TABLE zen_auth_verifications ( + id text NOT NULL PRIMARY KEY, + identifier text NOT NULL, + value text NOT NULL, + token text NOT NULL, + expires_at timestamptz NOT NULL, + created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL + ) + ` + } + ]; + + const created = []; + const skipped = []; + + for (const table of tables) { + const exists = await tableExists(table.name); + + if (!exists) { + await query(table.sql); + created.push(table.name); + console.log(`✓ Created table: ${table.name}`); + } else { + skipped.push(table.name); + console.log(`- Table already exists: ${table.name}`); + } + } + + return { + created, + skipped, + success: true + }; +} + +/** + * Initialize the database with all required tables + * @returns {Promise} Result object with created and skipped tables + */ +async function initDatabase() { + console.log('Initializing Zen database...'); + + try { + const authResult = await createAuthTables(); + + // Initialize modules + let modulesResult = { created: [], skipped: [] }; + try { + const { initModules } = await import('../../modules/init.js'); + modulesResult = await initModules(); + } catch (error) { + // Modules might not be available or enabled + console.log('\nNo modules to initialize or modules not available.'); + } + + console.log('\nDatabase initialization completed!'); + console.log(`Auth tables created: ${authResult.created.length}`); + console.log(`Module tables created: ${modulesResult.created.length}`); + console.log(`Total tables skipped: ${authResult.skipped.length + modulesResult.skipped.length}`); + + return { + created: [...authResult.created, ...modulesResult.created], + skipped: [...authResult.skipped, ...modulesResult.skipped], + success: true + }; + } catch (error) { + console.error('Database initialization failed:', error); + throw error; + } +} + +/** + * Drop all Zen authentication tables (use with caution!) + * @returns {Promise} + */ +async function dropAuthTables() { + const tables = [ + 'zen_auth_verifications', + 'zen_auth_accounts', + 'zen_auth_sessions', + 'zen_auth_users' + ]; + + console.log('WARNING: Dropping all Zen authentication tables...'); + + for (const tableName of tables) { + const exists = await tableExists(tableName); + if (exists) { + await query(`DROP TABLE IF EXISTS "${tableName}" CASCADE`); + console.log(`✓ Dropped table: ${tableName}`); + } + } + + console.log('All authentication tables dropped.'); +} + +export { + initDatabase, + createAuthTables, + tableExists, + dropAuthTables +}; + diff --git a/src/core/email/index.js b/src/core/email/index.js new file mode 100644 index 0000000..5721721 --- /dev/null +++ b/src/core/email/index.js @@ -0,0 +1,210 @@ +/** + * Email Utility using Resend + * Centralized email sending functionality for the entire package + */ + +import { Resend } from 'resend'; + +/** + * Initialize Resend client + */ +let resendClient = null; + +function getResendClient() { + if (!resendClient) { + const apiKey = process.env.ZEN_EMAIL_RESEND_APIKEY; + if (!apiKey) { + throw new Error('ZEN_EMAIL_RESEND_APIKEY environment variable is not set'); + } + resendClient = new Resend(apiKey); + } + return resendClient; +} + +/** + * Format sender address with name if available + * @param {string} email - Email address + * @param {string} name - Sender name (optional) + * @returns {string} Formatted sender address + */ +function formatSenderAddress(email, name) { + if (name && name.trim()) { + return `${name.trim()} <${email}>`; + } + return email; +} + +/** + * Send an email using Resend + * @param {Object} options - Email options + * @param {string} options.to - Recipient email address + * @param {string} options.subject - Email subject + * @param {string} options.html - HTML content of the email + * @param {string} options.text - Plain text content of the email (optional) + * @param {string} options.from - Sender email address (optional, defaults to ZEN_EMAIL_FROM_ADDRESS) + * @param {string} options.fromName - Sender name (optional, defaults to ZEN_EMAIL_FROM_NAME) + * @param {string} options.replyTo - Reply-to email address (optional) + * @param {Array} options.attachments - Email attachments (optional) + * @param {Object} options.tags - Email tags for tracking (optional) + * @returns {Promise} Resend response + */ +async function sendEmail({ to, subject, html, text, from, fromName, replyTo, attachments, tags }) { + try { + const resend = getResendClient(); + + // Default from address and name + const fromAddress = from || process.env.ZEN_EMAIL_FROM_ADDRESS || 'noreply@example.com'; + const senderName = fromName || process.env.ZEN_EMAIL_FROM_NAME; + + // Format sender with name if available + const formattedFrom = formatSenderAddress(fromAddress, senderName); + + const emailData = { + from: formattedFrom, + to, + subject, + html, + ...(text && { text }), + ...(replyTo && { reply_to: replyTo }), + ...(attachments && { attachments }), + ...(tags && { tags }) + }; + + const response = await resend.emails.send(emailData); + + // Resend returns { data: { id: "..." }, error: null } or { data: null, error: { message: "..." } } + if (response.error) { + console.error('[ZEN EMAIL] Resend error:', response.error); + return { + success: false, + data: null, + error: response.error.message || 'Failed to send email' + }; + } + + const emailId = response.data?.id || response.id; + console.log(`[ZEN EMAIL] Email sent to ${to} - ID: ${emailId}`); + + return { + success: true, + data: response.data || response, + error: null + }; + } catch (error) { + console.error('[ZEN EMAIL] Error sending email:', error); + return { + success: false, + data: null, + error: error.message + }; + } +} + +/** + * Send an authentication-related email + * Uses default ZEN_EMAIL_FROM_ADDRESS and ZEN_EMAIL_FROM_NAME + * @param {Object} options - Email options + * @param {string} options.to - Recipient email address + * @param {string} options.subject - Email subject + * @param {string} options.html - HTML content of the email + * @param {string} options.text - Plain text content of the email (optional) + * @param {string} options.replyTo - Reply-to email address (optional) + * @returns {Promise} Resend response + */ +async function sendAuthEmail({ to, subject, html, text, replyTo }) { + return sendEmail({ + to, + subject, + html, + text, + replyTo + }); +} + +/** + * Send an application-related email + * Uses default ZEN_EMAIL_FROM_ADDRESS and ZEN_EMAIL_FROM_NAME + * @param {Object} options - Email options + * @param {string} options.to - Recipient email address + * @param {string} options.subject - Email subject + * @param {string} options.html - HTML content of the email + * @param {string} options.text - Plain text content of the email (optional) + * @param {string} options.replyTo - Reply-to email address (optional) + * @param {Array} options.attachments - Email attachments (optional) + * @param {Object} options.tags - Email tags for tracking (optional) + * @returns {Promise} Resend response + */ +async function sendAppEmail({ to, subject, html, text, replyTo, attachments, tags }) { + return sendEmail({ + to, + subject, + html, + text, + replyTo, + attachments, + tags + }); +} + +/** + * Send a batch of emails + * @param {Array} emails - Array of email objects + * @returns {Promise>} Array of Resend responses + */ +async function sendBatchEmails(emails) { + try { + const resend = getResendClient(); + + const emailsData = emails.map(email => { + const fromAddress = email.from || process.env.ZEN_EMAIL_FROM_ADDRESS || 'noreply@example.com'; + const fromName = email.fromName || process.env.ZEN_EMAIL_FROM_NAME; + const formattedFrom = formatSenderAddress(fromAddress, fromName); + + return { + from: formattedFrom, + to: email.to, + subject: email.subject, + html: email.html, + ...(email.text && { text: email.text }), + ...(email.replyTo && { reply_to: email.replyTo }), + ...(email.attachments && { attachments: email.attachments }), + ...(email.tags && { tags: email.tags }) + }; + }); + + const response = await resend.batch.send(emailsData); + + // Handle Resend error response + if (response.error) { + console.error('[ZEN EMAIL] Resend batch error:', response.error); + return { + success: false, + data: null, + error: response.error.message || 'Failed to send batch emails' + }; + } + + console.log(`[ZEN EMAIL] Batch of ${emails.length} emails sent`); + + return { + success: true, + data: response.data || response, + error: null + }; + } catch (error) { + console.error('[ZEN EMAIL] Error sending batch emails:', error); + return { + success: false, + data: null, + error: error.message + }; + } +} + +export { + sendEmail, + sendAuthEmail, + sendAppEmail, + sendBatchEmails +}; + diff --git a/src/core/email/templates/BaseLayout.jsx b/src/core/email/templates/BaseLayout.jsx new file mode 100644 index 0000000..9bb0022 --- /dev/null +++ b/src/core/email/templates/BaseLayout.jsx @@ -0,0 +1,86 @@ +/** + * Base Email Layout Component + * Provides consistent structure for all ZEN emails + */ + +import { + Body, + Container, + Head, + Heading, + Html, + Preview, + Section, + Text, + Img, + Tailwind, + Hr, + Link, +} from "@react-email/components"; + +export const BaseLayout = ({ + preview, + title, + children, + companyName, + logoURL, + supportSection = false, + supportEmail = 'support@zenya.test' +}) => { + const appName = companyName || process.env.ZEN_NAME || 'ZEN'; + const logoSrc = logoURL || process.env.ZEN_EMAIL_LOGO || null; + const logoHref = process.env.ZEN_EMAIL_LOGO_URL || null; + const currentYear = new Date().getFullYear(); + + return ( + + + {preview && {preview}} + + + + +
+ {logoSrc ? ( + logoHref ? ( + + {appName} + + ) : ( + {appName} + ) + ) : ( + + {appName} + + )} +
+ + {title && ( + + {title} + + )} + + {children} + +
+ + + © {currentYear} — {appName}. Tous droits réservés. + {supportSection && ( + <> + {' · '} + + {supportEmail} + + + )} + + +
+ +
+ + ); +}; diff --git a/src/core/email/templates/PasswordChangedEmail.jsx b/src/core/email/templates/PasswordChangedEmail.jsx new file mode 100644 index 0000000..33d944d --- /dev/null +++ b/src/core/email/templates/PasswordChangedEmail.jsx @@ -0,0 +1,41 @@ +/** + * Password Changed Confirmation Email Template + */ + +import { Section, Text } from "@react-email/components"; +import { BaseLayout } from "./BaseLayout"; + +export const PasswordChangedEmail = ({ + email, + companyName +}) => { + const appName = companyName || process.env.ZEN_NAME || 'ZEN'; + const supportEmail = process.env.ZEN_SUPPORT_EMAIL || 'support@zenya.test'; + + return ( + + + Ceci confirme que le mot de passe de votre compte {appName} a bien été modifié. + + +
+ + Compte + + + {email} + +
+ + + Si vous n'êtes pas à l'origine de cette modification, contactez immédiatement notre équipe de support. + +
+ ); +}; diff --git a/src/core/email/templates/PasswordResetEmail.jsx b/src/core/email/templates/PasswordResetEmail.jsx new file mode 100644 index 0000000..a1a16ce --- /dev/null +++ b/src/core/email/templates/PasswordResetEmail.jsx @@ -0,0 +1,49 @@ +/** + * Password Reset Email Template + */ + +import { Button, Section, Text, Link } from "@react-email/components"; +import { BaseLayout } from "./BaseLayout"; + +export const PasswordResetEmail = ({ + email, + resetUrl, + companyName +}) => { + const appName = companyName || process.env.ZEN_NAME || 'ZEN'; + const supportEmail = process.env.ZEN_SUPPORT_EMAIL || 'support@zenya.test'; + + return ( + + + Nous avons reçu une demande de réinitialisation du mot de passe pour votre compte {appName}. Cliquez sur le bouton ci-dessous pour en choisir un nouveau. + + +
+ +
+ + + Ce lien expire dans 1 heure. Si vous n'êtes pas à l'origine de cette demande, ignorez ce message — votre mot de passe ne sera pas modifié. + + + + Lien :{' '} + + {resetUrl} + + +
+ ); +}; diff --git a/src/core/email/templates/VerificationEmail.jsx b/src/core/email/templates/VerificationEmail.jsx new file mode 100644 index 0000000..344067d --- /dev/null +++ b/src/core/email/templates/VerificationEmail.jsx @@ -0,0 +1,49 @@ +/** + * Email Verification Template + */ + +import { Button, Section, Text, Link } from "@react-email/components"; +import { BaseLayout } from "./BaseLayout"; + +export const VerificationEmail = ({ + email, + verificationUrl, + companyName +}) => { + const appName = companyName || process.env.ZEN_NAME || 'ZEN'; + const supportEmail = process.env.ZEN_SUPPORT_EMAIL || 'support@zenya.test'; + + return ( + + + Merci de vous être inscrit sur {appName}. Cliquez sur le bouton ci-dessous pour confirmer votre adresse courriel. + + +
+ +
+ + + Ce lien expire dans 24 heures. Si vous n'avez pas créé de compte, ignorez ce message. + + + + Lien :{' '} + + {verificationUrl} + + +
+ ); +}; diff --git a/src/core/email/templates/index.js b/src/core/email/templates/index.js new file mode 100644 index 0000000..f416894 --- /dev/null +++ b/src/core/email/templates/index.js @@ -0,0 +1,71 @@ +/** + * Email Templates + * Export all email templates and render functions + */ + +import { render } from '@react-email/components'; +import { VerificationEmail } from './VerificationEmail.jsx'; +import { PasswordResetEmail } from './PasswordResetEmail.jsx'; +import { PasswordChangedEmail } from './PasswordChangedEmail.jsx'; + +// Export JSX components +export { VerificationEmail } from './VerificationEmail.jsx'; +export { PasswordResetEmail } from './PasswordResetEmail.jsx'; +export { PasswordChangedEmail } from './PasswordChangedEmail.jsx'; +export { BaseLayout } from './BaseLayout.jsx'; + +/** + * Render verification email to HTML + * @param {string} verificationUrl - The verification URL + * @param {string} email - User's email address + * @param {string} companyName - Company name (optional) + * @returns {Promise} Rendered HTML + */ +export async function renderVerificationEmail(verificationUrl, email, companyName) { + return await render( + + ); +} + +/** + * Render password reset email to HTML + * @param {string} resetUrl - The password reset URL + * @param {string} email - User's email address + * @param {string} companyName - Company name (optional) + * @returns {Promise} Rendered HTML + */ +export async function renderPasswordResetEmail(resetUrl, email, companyName) { + return await render( + + ); +} + +/** + * Render password changed email to HTML + * @param {string} email - User's email address + * @param {string} companyName - Company name (optional) + * @returns {Promise} Rendered HTML + */ +export async function renderPasswordChangedEmail(email, companyName) { + return await render( + + ); +} + +// Legacy exports for backward compatibility +export const getVerificationEmailTemplate = renderVerificationEmail; +export const getPasswordResetTemplate = renderPasswordResetEmail; +export const getPasswordChangedTemplate = renderPasswordChangedEmail; + + diff --git a/src/core/modules/client.js b/src/core/modules/client.js new file mode 100644 index 0000000..34588fd --- /dev/null +++ b/src/core/modules/client.js @@ -0,0 +1,32 @@ +/** + * Client-Safe Module Registry Access + * + * This file ONLY exports functions that are safe to use in client components. + * It does NOT export discovery, loader, or initialization functions that + * might import server-only modules like database code. + * + * NOTE: Most registry functions return empty results on the client because + * the registry is populated on the server during discovery. For client-side + * module page loading, use the loaders from modules.pages.js instead. + */ + +// Only export registry getter functions (no discovery/loader functions) +export { + getModule, + getAllModules, + getEnabledModules, + isModuleRegistered, + isModuleEnabled, + getAllApiRoutes, + getAllAdminNavigation, + getAdminPage, + getAllCronJobs, + getAllPublicRoutes, + getAllDatabaseSchemas, + getModuleMetadata, + getAllModuleMetadata, +} from './registry.js'; + +// NOTE: getModulePublicPages is NOT exported here because it relies on the +// server-side registry which is empty on the client. Use getModulePublicPageLoader() +// from '@hykocx/zen/modules/pages' instead for client-side public page loading. diff --git a/src/core/modules/discovery.js b/src/core/modules/discovery.js new file mode 100644 index 0000000..fb8da86 --- /dev/null +++ b/src/core/modules/discovery.js @@ -0,0 +1,192 @@ +/** + * Module Discovery System + * Auto-discovers and registers modules from the modules directory + */ + +import { registerModule, clearRegistry } from './registry.js'; +import { getAvailableModules } from '../../modules/modules.registry.js'; + +/** + * Check if a module is enabled via environment variable + * @param {string} moduleName - Module name + * @returns {boolean} + */ +export function isModuleEnabledInEnv(moduleName) { + const envVar = `ZEN_MODULE_${moduleName.toUpperCase()}`; + return process.env[envVar] === 'true'; +} + +/** + * Discover and register all modules + * @param {Object} options - Discovery options + * @param {boolean} options.force - Force re-discovery + * @returns {Promise} Discovery result + */ +export async function discoverModules(options = {}) { + const { force = false } = options; + + const DISCOVERY_KEY = Symbol.for('__ZEN_MODULES_DISCOVERED__'); + + if (globalThis[DISCOVERY_KEY] && !force) { + console.log('[Module Discovery] Already discovered, skipping...'); + return { alreadyDiscovered: true }; + } + + if (force) { + clearRegistry(); + } + + console.log('[Module Discovery] Starting module discovery...'); + + const discovered = []; + const enabled = []; + const skipped = []; + const errors = []; + + const knownModules = getAvailableModules(); + + for (const moduleName of knownModules) { + try { + const isEnabled = isModuleEnabledInEnv(moduleName); + + if (!isEnabled) { + skipped.push(moduleName); + console.log(`[Module Discovery] Skipped ${moduleName} (not enabled)`); + continue; + } + + // Load module configuration + const moduleConfig = await loadModuleConfig(moduleName); + + if (moduleConfig) { + // Load additional components (db, cron, api) + const components = await loadModuleComponents(moduleName); + + // Register the module + registerModule(moduleName, { + ...moduleConfig, + ...components, + enabled: true + }); + + discovered.push(moduleName); + enabled.push(moduleName); + console.log(`[Module Discovery] Registered ${moduleName}`); + } + } catch (error) { + errors.push({ module: moduleName, error: error.message }); + console.error(`[Module Discovery] Error loading ${moduleName}:`, error); + } + } + + globalThis[DISCOVERY_KEY] = true; + + console.log(`[Module Discovery] Complete. Enabled: ${enabled.length}, Skipped: ${skipped.length}, Errors: ${errors.length}`); + + return { discovered, enabled, skipped, errors }; +} + +/** + * Load module configuration from module.config.js + * @param {string} moduleName - Module name + * @returns {Promise} Module configuration + */ +async function loadModuleConfig(moduleName) { + try { + const config = await import(`../../modules/${moduleName}/module.config.js`); + const moduleConfig = config.default || config; + + // Build admin config with navigation and pages + let adminConfig = undefined; + if (moduleConfig.navigation || moduleConfig.adminPages) { + adminConfig = {}; + if (moduleConfig.navigation) { + adminConfig.navigation = moduleConfig.navigation; + } + // Extract admin page paths (keys only, not the lazy components) + // This allows getAdminPage() to know which paths belong to this module + if (moduleConfig.adminPages) { + adminConfig.pages = {}; + for (const path of Object.keys(moduleConfig.adminPages)) { + // Store true as a marker that this path exists + // The actual component is loaded client-side via modules.pages.js + adminConfig.pages[path] = true; + } + } + } + + // Extract server-side relevant data + return { + name: moduleConfig.name || moduleName, + displayName: moduleConfig.displayName || moduleName.charAt(0).toUpperCase() + moduleName.slice(1), + version: moduleConfig.version || '1.0.0', + description: moduleConfig.description || `${moduleName} module`, + dependencies: moduleConfig.dependencies || [], + envVars: moduleConfig.envVars || [], + // Admin configuration (navigation + page paths) + admin: adminConfig, + // Public routes metadata (not components) + public: moduleConfig.publicRoutes ? { + routes: moduleConfig.publicRoutes + } : undefined, + }; + } catch (error) { + console.log(`[Module Discovery] No module.config.js for ${moduleName}, using defaults`); + return { + name: moduleName, + displayName: moduleName.charAt(0).toUpperCase() + moduleName.slice(1), + version: '1.0.0', + description: `${moduleName} module` + }; + } +} + +/** + * Load additional module components (db, cron, api) + * Note: Metadata is loaded from modules.metadata.js (static registry) + * @param {string} moduleName - Module name + * @returns {Promise} Module components + */ +async function loadModuleComponents(moduleName) { + const components = {}; + + // Load API routes + try { + const api = await import(`../../modules/${moduleName}/api.js`); + components.api = api.default || api; + } catch (error) { + // API is optional + } + + // Load cron configuration + try { + const cron = await import(`../../modules/${moduleName}/cron.config.js`); + components.cron = cron.default || cron; + } catch (error) { + // Cron is optional + } + + // Load database configuration + try { + const db = await import(`../../modules/${moduleName}/db.js`); + if (db.createTables) { + components.db = { + init: db.createTables, + drop: db.dropTables + }; + } + } catch (error) { + // DB is optional + } + + return components; +} + +/** + * Reset module discovery (useful for testing) + */ +export function resetDiscovery() { + const DISCOVERY_KEY = Symbol.for('__ZEN_MODULES_DISCOVERED__'); + globalThis[DISCOVERY_KEY] = false; + clearRegistry(); +} diff --git a/src/core/modules/index.js b/src/core/modules/index.js new file mode 100644 index 0000000..6737e50 --- /dev/null +++ b/src/core/modules/index.js @@ -0,0 +1,42 @@ +/** + * Module System Entry Point + * Exports all module-related functionality + */ + +// Discovery +export { + discoverModules, + isModuleEnabledInEnv, + resetDiscovery +} from './discovery.js'; + +// Registry (server-side only - these functions rely on the registry populated during discovery) +export { + registerModule, + getModule, + getAllModules, + getEnabledModules, + isModuleRegistered, + isModuleEnabled, + clearRegistry, + getAllApiRoutes, + getAllAdminNavigation, + getAdminPage, + getAllCronJobs, + getAllPublicRoutes, + getAllDatabaseSchemas, + getModuleMetadata, + getAllModuleMetadata, + getModulePublicPages // returns route metadata only, use modules.pages.js for components +} from './registry.js'; + +// Loader +export { + initializeModules, + initializeModuleDatabases, + startModuleCronJobs, + stopModuleCronJobs, + getCronJobStatus, + resetModuleLoader, + getModuleStatus +} from './loader.js'; diff --git a/src/core/modules/loader.js b/src/core/modules/loader.js new file mode 100644 index 0000000..3cc15c0 --- /dev/null +++ b/src/core/modules/loader.js @@ -0,0 +1,244 @@ +/** + * Module Loader + * Handles loading and initializing modules + */ + +import { discoverModules, resetDiscovery } from './discovery.js'; +import { + getAllModules, + getEnabledModules, + getAllCronJobs, + getAllDatabaseSchemas, + isModuleEnabled +} from './registry.js'; + +// Use globalThis to track initialization state +const INIT_KEY = Symbol.for('__ZEN_MODULES_INITIALIZED__'); +const CRON_JOBS_KEY = Symbol.for('__ZEN_MODULE_CRON_JOBS__'); + +/** + * Initialize all modules + * Discovers modules, initializes databases, and starts cron jobs + * @param {Object} options - Initialization options + * @param {boolean} options.skipCron - Skip starting cron jobs + * @param {boolean} options.skipDb - Skip database initialization + * @param {boolean} options.force - Force re-initialization + * @returns {Promise} Initialization result + */ +export async function initializeModules(options = {}) { + const { skipCron = false, skipDb = false, force = false } = options; + + // Prevent multiple initializations + if (globalThis[INIT_KEY] && !force) { + console.log('[Module Loader] Already initialized, skipping...'); + return { alreadyInitialized: true }; + } + + console.log('[Module Loader] Starting module initialization...'); + + const result = { + discovery: null, + database: { created: [], skipped: [], errors: [] }, + cron: { started: [], errors: [] } + }; + + try { + // Step 1: Discover modules + result.discovery = await discoverModules({ force }); + + // Step 2: Initialize databases + if (!skipDb) { + result.database = await initializeModuleDatabases(); + } + + // Step 3: Start cron jobs + if (!skipCron) { + result.cron = await startModuleCronJobs(); + } + + globalThis[INIT_KEY] = true; + console.log('[Module Loader] Module initialization complete'); + + } catch (error) { + console.error('[Module Loader] Initialization failed:', error); + result.error = error.message; + } + + return result; +} + +/** + * Initialize databases for all enabled modules + * @returns {Promise} Database initialization result + */ +export async function initializeModuleDatabases() { + console.log('[Module Loader] Initializing module databases...'); + + const schemas = getAllDatabaseSchemas(); + const result = { + created: [], + skipped: [], + errors: [] + }; + + for (const schema of schemas) { + try { + if (schema.init && typeof schema.init === 'function') { + const initResult = await schema.init(); + + if (initResult?.created) { + result.created.push(...initResult.created); + } + if (initResult?.skipped) { + result.skipped.push(...initResult.skipped); + } + + console.log(`[Module Loader] Database initialized for ${schema.module}`); + } + } catch (error) { + result.errors.push({ + module: schema.module, + error: error.message + }); + console.error(`[Module Loader] Database init error for ${schema.module}:`, error); + } + } + + return result; +} + +/** + * Start cron jobs for all enabled modules + * @returns {Promise} Cron job start result + */ +export async function startModuleCronJobs() { + console.log('[Module Loader] Starting module cron jobs...'); + + // Stop existing cron jobs first + stopModuleCronJobs(); + + const jobs = getAllCronJobs(); + const result = { + started: [], + errors: [] + }; + + // Initialize cron jobs storage + if (!globalThis[CRON_JOBS_KEY]) { + globalThis[CRON_JOBS_KEY] = new Map(); + } + + for (const job of jobs) { + try { + if (job.handler && typeof job.handler === 'function') { + // Dynamic import of node-cron + const cron = (await import('node-cron')).default; + + const cronJob = cron.schedule(job.schedule, async () => { + console.log(`[Cron: ${job.name}] Running at:`, new Date().toISOString()); + try { + await job.handler(); + console.log(`[Cron: ${job.name}] Completed`); + } catch (error) { + console.error(`[Cron: ${job.name}] Error:`, error); + } + }, { + scheduled: true, + timezone: job.timezone || process.env.ZEN_TIMEZONE || 'America/Toronto' + }); + + globalThis[CRON_JOBS_KEY].set(job.name, cronJob); + result.started.push(job.name); + + console.log(`[Module Loader] Started cron job: ${job.name} (${job.schedule})`); + } + } catch (error) { + result.errors.push({ + job: job.name, + module: job.module, + error: error.message + }); + console.error(`[Module Loader] Cron job error for ${job.name}:`, error); + } + } + + return result; +} + +/** + * Stop all module cron jobs + */ +export function stopModuleCronJobs() { + if (globalThis[CRON_JOBS_KEY]) { + for (const [name, job] of globalThis[CRON_JOBS_KEY].entries()) { + try { + job.stop(); + console.log(`[Module Loader] Stopped cron job: ${name}`); + } catch (error) { + console.error(`[Module Loader] Error stopping cron job ${name}:`, error); + } + } + globalThis[CRON_JOBS_KEY].clear(); + } +} + +/** + * Get status of all cron jobs + * @returns {Object} Cron job status + */ +export function getCronJobStatus() { + const status = {}; + + if (globalThis[CRON_JOBS_KEY]) { + for (const [name, job] of globalThis[CRON_JOBS_KEY].entries()) { + status[name] = { + running: true // node-cron doesn't expose a running state easily + }; + } + } + + return status; +} + +/** + * Reset module loader (useful for testing) + */ +export function resetModuleLoader() { + stopModuleCronJobs(); + resetDiscovery(); + globalThis[INIT_KEY] = false; +} + +/** + * Get module status + * @returns {Object} Status of all modules + */ +export function getModuleStatus() { + const modules = getAllModules(); + const enabled = getEnabledModules(); + const cronStatus = getCronJobStatus(); + + return { + totalModules: modules.size, + enabledModules: enabled.length, + modules: Array.from(modules.entries()).map(([name, data]) => ({ + name, + enabled: data.enabled, + displayName: data.displayName, + version: data.version, + hasApi: !!data.api, + hasAdmin: !!data.admin, + hasCron: !!data.cron, + hasDb: !!data.db, + hasPublic: !!data.public + })), + cronJobs: cronStatus + }; +} + +// Re-export useful functions from registry +export { + isModuleEnabled, + getAllModules, + getEnabledModules +} from './registry.js'; diff --git a/src/core/modules/registry.js b/src/core/modules/registry.js new file mode 100644 index 0000000..c718b04 --- /dev/null +++ b/src/core/modules/registry.js @@ -0,0 +1,289 @@ +/** + * Module Registry + * Stores and manages all discovered modules + */ + +// Use globalThis to persist registry across module reloads +const REGISTRY_KEY = Symbol.for('__ZEN_MODULE_REGISTRY__'); + +/** + * Initialize or get the module registry + * @returns {Map} Module registry map + */ +function getRegistry() { + if (!globalThis[REGISTRY_KEY]) { + globalThis[REGISTRY_KEY] = new Map(); + } + return globalThis[REGISTRY_KEY]; +} + +/** + * Register a module in the registry + * @param {string} name - Module name + * @param {Object} moduleData - Module configuration and components + */ +export function registerModule(name, moduleData) { + const registry = getRegistry(); + registry.set(name, { + ...moduleData, + registeredAt: new Date().toISOString() + }); +} + +/** + * Get a registered module by name + * @param {string} name - Module name + * @returns {Object|null} Module data or null + */ +export function getModule(name) { + const registry = getRegistry(); + return registry.get(name) || null; +} + +/** + * Get all registered modules + * @returns {Map} All registered modules + */ +export function getAllModules() { + return getRegistry(); +} + +/** + * Get all enabled modules + * @returns {Array} Array of enabled module data + */ +export function getEnabledModules() { + const registry = getRegistry(); + const enabled = []; + + for (const [name, data] of registry.entries()) { + if (data.enabled) { + enabled.push({ name, ...data }); + } + } + + return enabled; +} + +/** + * Check if a module is registered + * @param {string} name - Module name + * @returns {boolean} + */ +export function isModuleRegistered(name) { + const registry = getRegistry(); + return registry.has(name); +} + +/** + * Check if a module is enabled + * @param {string} name - Module name + * @returns {boolean} + */ +export function isModuleEnabled(name) { + const module = getModule(name); + return module?.enabled === true; +} + +/** + * Clear the module registry (useful for testing) + */ +export function clearRegistry() { + const registry = getRegistry(); + registry.clear(); +} + +/** + * Get all API routes from enabled modules + * @returns {Array} Array of route definitions + */ +export function getAllApiRoutes() { + const routes = []; + const registry = getRegistry(); + + for (const [name, data] of registry.entries()) { + if (data.enabled && data.api?.routes) { + routes.push(...data.api.routes.map(route => ({ + ...route, + module: name + }))); + } + } + + return routes; +} + +/** + * Get all admin navigation sections from enabled modules + * @param {string} pathname - Current pathname for active state + * @returns {Array} Array of navigation sections + */ +export function getAllAdminNavigation(pathname) { + const sections = []; + const registry = getRegistry(); + + for (const [name, data] of registry.entries()) { + if (data.enabled && data.admin?.navigation) { + const nav = data.admin.navigation; + + // Handle function or object navigation + const section = typeof nav === 'function' ? nav(pathname) : nav; + + if (section) { + // Support array of sections (e.g. one per post type) + const sectionList = Array.isArray(section) ? section : [section]; + for (const s of sectionList) { + if (s.items) { + s.items = s.items.map(item => ({ + ...item, + current: pathname.startsWith(item.href) + })); + } + sections.push({ ...s, module: name }); + } + } + } + } + + return sections; +} + +/** + * Get admin page info for a given path + * + * Returns module info if the path is registered as an admin page. + * The actual component is loaded client-side via modules.pages.js + * + * @param {string} path - Page path (e.g., '/admin/invoice/invoices') + * @returns {Object|null} Object with { module, path } or null + */ +export function getAdminPage(path) { + const registry = getRegistry(); + + for (const [name, data] of registry.entries()) { + if (data.enabled && data.admin?.pages) { + if (data.admin.pages[path]) { + return { module: name, path }; + } + } + } + + return null; +} + +/** + * Get all cron jobs from enabled modules + * @returns {Array} Array of cron job definitions + */ +export function getAllCronJobs() { + const jobs = []; + const registry = getRegistry(); + + for (const [name, data] of registry.entries()) { + if (data.enabled && data.cron?.jobs) { + jobs.push(...data.cron.jobs.map(job => ({ + ...job, + module: name + }))); + } + } + + return jobs; +} + +/** + * Get public routes from enabled modules + * @returns {Array} Array of public route definitions + */ +export function getAllPublicRoutes() { + const routes = []; + const registry = getRegistry(); + + for (const [name, data] of registry.entries()) { + if (data.enabled && data.public?.routes) { + routes.push(...data.public.routes.map(route => ({ + ...route, + module: name + }))); + } + } + + return routes; +} + +/** + * Get database schemas from all enabled modules + * @returns {Array} Array of database schema definitions + */ +export function getAllDatabaseSchemas() { + const schemas = []; + const registry = getRegistry(); + + for (const [name, data] of registry.entries()) { + if (data.enabled && data.db) { + schemas.push({ + module: name, + ...data.db + }); + } + } + + return schemas; +} + +/** + * Get metadata generator function from a module + * @param {string} moduleName - Module name (e.g., 'invoice') + * @param {string} type - Metadata type (e.g., 'payment', 'pdf', 'receipt') + * @returns {Function|null} Metadata generator function or null if not found + */ +export function getModuleMetadata(moduleName, type) { + const module = getModule(moduleName); + + if (module?.enabled && module?.metadata) { + // If type is specified, return the specific generator + if (type && module.metadata[type]) { + return module.metadata[type]; + } + // If no type, return the default (first one or 'payment') + return module.metadata.payment || module.metadata[Object.keys(module.metadata)[0]] || null; + } + + return null; +} + +/** + * Get all metadata configurations from enabled modules + * @returns {Object} Object mapping module names to their metadata configs + */ +export function getAllModuleMetadata() { + const metadata = {}; + const registry = getRegistry(); + + for (const [name, data] of registry.entries()) { + if (data.enabled && data.metadata) { + metadata[name] = data.metadata; + } + } + + return metadata; +} + +/** + * Get public routes configuration from a module + * + * NOTE: This function only returns route metadata, not components. + * For loading public page components, use getModulePublicPageLoader() from modules.pages.js + * + * @param {string} moduleName - Module name + * @returns {Object|null} Public routes config or null + */ +export function getModulePublicPages(moduleName) { + const module = getModule(moduleName); + + if (module?.enabled && module?.public) { + return module.public; + } + + return null; +} diff --git a/src/core/payments/index.js b/src/core/payments/index.js new file mode 100644 index 0000000..074977c --- /dev/null +++ b/src/core/payments/index.js @@ -0,0 +1,7 @@ +/** + * Payments Module Entry Point + * Re-exports all payment utilities + */ + +export * from './stripe.js'; +export { default as stripe } from './stripe.js'; diff --git a/src/core/payments/stripe.js b/src/core/payments/stripe.js new file mode 100644 index 0000000..bcb4558 --- /dev/null +++ b/src/core/payments/stripe.js @@ -0,0 +1,270 @@ +/** + * Stripe Payment Utilities + * Generic Stripe integration for payment processing + * + * Usage in modules: + * import { createCheckoutSession, isEnabled } from '@hykocx/zen/stripe'; + */ + +/** + * Get Stripe instance + * @returns {Promise} Stripe instance + */ +export async function getStripe() { + const secretKey = process.env.STRIPE_SECRET_KEY; + + if (!secretKey) { + throw new Error('Stripe is not configured. Please set STRIPE_SECRET_KEY environment variable.'); + } + + const Stripe = (await import('stripe')).default; + return new Stripe(secretKey, { + apiVersion: '2023-10-16', + }); +} + +/** + * Check if Stripe is enabled + * @returns {boolean} + */ +export function isEnabled() { + return !!(process.env.STRIPE_SECRET_KEY && process.env.STRIPE_PUBLISHABLE_KEY); +} + +/** + * Get Stripe publishable key (for client-side) + * @returns {string|null} + */ +export function getPublishableKey() { + return process.env.STRIPE_PUBLISHABLE_KEY || null; +} + +/** + * Create a checkout session + * @param {Object} options - Checkout options + * @param {Array} options.lineItems - Line items for checkout + * @param {string} options.successUrl - Success redirect URL + * @param {string} options.cancelUrl - Cancel redirect URL + * @param {string} options.customerEmail - Customer email + * @param {Object} options.metadata - Additional metadata + * @param {string} options.mode - Payment mode (default: 'payment') + * @returns {Promise} Stripe session object + * + * @example + * const session = await createCheckoutSession({ + * lineItems: [{ + * price_data: { + * currency: 'usd', + * product_data: { name: 'Product' }, + * unit_amount: 1000, + * }, + * quantity: 1, + * }], + * successUrl: 'https://example.com/success', + * cancelUrl: 'https://example.com/cancel', + * }); + */ +export async function createCheckoutSession(options) { + const stripe = await getStripe(); + + const { + lineItems, + successUrl, + cancelUrl, + customerEmail, + metadata = {}, + mode = 'payment', + paymentMethodTypes = ['card'], + clientReferenceId, + } = options; + + const sessionConfig = { + payment_method_types: paymentMethodTypes, + line_items: lineItems, + mode, + success_url: successUrl, + cancel_url: cancelUrl, + metadata, + }; + + if (customerEmail) { + sessionConfig.customer_email = customerEmail; + } + + if (clientReferenceId) { + sessionConfig.client_reference_id = clientReferenceId; + } + + return await stripe.checkout.sessions.create(sessionConfig); +} + +/** + * Create a payment intent + * @param {Object} options - Payment options + * @param {number} options.amount - Amount in cents + * @param {string} options.currency - Currency code + * @param {Object} options.metadata - Additional metadata + * @returns {Promise} Stripe payment intent + */ +export async function createPaymentIntent(options) { + const stripe = await getStripe(); + + const { + amount, + currency = process.env.ZEN_CURRENCY?.toLowerCase() || 'cad', + metadata = {}, + automaticPaymentMethods = { enabled: true }, + } = options; + + return await stripe.paymentIntents.create({ + amount, + currency, + metadata, + automatic_payment_methods: automaticPaymentMethods, + }); +} + +/** + * Retrieve a checkout session + * @param {string} sessionId - Session ID + * @returns {Promise} Stripe session + */ +export async function getCheckoutSession(sessionId) { + const stripe = await getStripe(); + return await stripe.checkout.sessions.retrieve(sessionId); +} + +/** + * Retrieve a payment intent + * @param {string} paymentIntentId - Payment intent ID + * @returns {Promise} Stripe payment intent + */ +export async function getPaymentIntent(paymentIntentId) { + const stripe = await getStripe(); + return await stripe.paymentIntents.retrieve(paymentIntentId); +} + +/** + * Verify webhook signature + * @param {string} payload - Raw request body + * @param {string} signature - Stripe-Signature header + * @param {string} secret - Webhook secret (optional, uses env if not provided) + * @returns {Promise} Verified event + */ +export async function verifyWebhookSignature(payload, signature, secret = null) { + const stripe = await getStripe(); + const webhookSecret = secret || process.env.STRIPE_WEBHOOK_SECRET; + + if (!webhookSecret) { + throw new Error('Stripe webhook secret is not configured'); + } + + return stripe.webhooks.constructEvent(payload, signature, webhookSecret); +} + +/** + * Create a customer + * @param {Object} options - Customer options + * @param {string} options.email - Customer email + * @param {string} options.name - Customer name + * @param {Object} options.metadata - Additional metadata + * @returns {Promise} Stripe customer + */ +export async function createCustomer(options) { + const stripe = await getStripe(); + + const { email, name, metadata = {} } = options; + + return await stripe.customers.create({ + email, + name, + metadata, + }); +} + +/** + * Get or create a customer by email + * @param {string} email - Customer email + * @param {Object} defaultData - Default data if creating new customer + * @returns {Promise} Stripe customer + */ +export async function getOrCreateCustomer(email, defaultData = {}) { + const stripe = await getStripe(); + + // Search for existing customer + const existing = await stripe.customers.list({ + email, + limit: 1, + }); + + if (existing.data.length > 0) { + return existing.data[0]; + } + + // Create new customer + return await stripe.customers.create({ + email, + ...defaultData, + }); +} + +/** + * List customer's payment methods + * @param {string} customerId - Customer ID + * @param {string} type - Payment method type (default: 'card') + * @returns {Promise} List of payment methods + */ +export async function listPaymentMethods(customerId, type = 'card') { + const stripe = await getStripe(); + + const methods = await stripe.paymentMethods.list({ + customer: customerId, + type, + }); + + return methods.data; +} + +/** + * Create a refund + * @param {Object} options - Refund options + * @param {string} options.paymentIntentId - Payment intent to refund + * @param {number} options.amount - Amount to refund in cents (optional, full refund if not specified) + * @param {string} options.reason - Reason for refund + * @returns {Promise} Stripe refund + */ +export async function createRefund(options) { + const stripe = await getStripe(); + + const { paymentIntentId, amount, reason } = options; + + const refundConfig = { + payment_intent: paymentIntentId, + }; + + if (amount) { + refundConfig.amount = amount; + } + + if (reason) { + refundConfig.reason = reason; + } + + return await stripe.refunds.create(refundConfig); +} + +// Default export for convenience +export default { + getStripe, + isEnabled, + getPublishableKey, + createCheckoutSession, + createPaymentIntent, + getCheckoutSession, + getPaymentIntent, + verifyWebhookSignature, + createCustomer, + getOrCreateCustomer, + listPaymentMethods, + createRefund, +}; diff --git a/src/core/pdf/index.js b/src/core/pdf/index.js new file mode 100644 index 0000000..9548dc6 --- /dev/null +++ b/src/core/pdf/index.js @@ -0,0 +1,121 @@ +/** + * PDF Generation Utilities + * Wrapper around @react-pdf/renderer for PDF generation + * + * Usage in modules: + * import { renderToBuffer } from '@hykocx/zen/pdf'; + */ + +import { renderToBuffer as reactPdfRenderToBuffer } from '@react-pdf/renderer'; +import React from 'react'; + +/** + * Render a React PDF document to a buffer + * @param {React.Element} document - React PDF document element + * @returns {Promise} PDF buffer + * + * @example + * import { Document, Page, Text } from '@react-pdf/renderer'; + * + * const MyDoc = () => ( + * + * + * Hello World + * + * + * ); + * + * const buffer = await renderToBuffer(); + */ +export async function renderToBuffer(document) { + return await reactPdfRenderToBuffer(document); +} + +/** + * Create a React element for PDF rendering + * @param {Function} Component - React component + * @param {Object} props - Component props + * @returns {React.Element} + */ +export function createElement(Component, props) { + return React.createElement(Component, props); +} + +/** + * Get a suggested filename for a PDF + * @param {string} prefix - Filename prefix + * @param {string|number} identifier - Unique identifier + * @param {Date} date - Date for the filename (default: today) + * @returns {string} Suggested filename + * + * @example + * getFilename('invoice', '12345'); // 'invoice-12345-2024-01-15.pdf' + */ +export function getFilename(prefix, identifier, date = new Date()) { + const dateStr = date.toISOString().split('T')[0]; + return `${prefix}-${identifier}-${dateStr}.pdf`; +} + +/** + * Convert centimeters to points (for PDF dimensions) + * @param {number} cm - Centimeters + * @returns {number} Points + */ +export function cmToPoints(cm) { + return cm * 28.3465; +} + +/** + * Convert inches to points (for PDF dimensions) + * @param {number} inches - Inches + * @returns {number} Points + */ +export function inchesToPoints(inches) { + return inches * 72; +} + +/** + * Convert millimeters to points (for PDF dimensions) + * @param {number} mm - Millimeters + * @returns {number} Points + */ +export function mmToPoints(mm) { + return mm * 2.83465; +} + +/** + * Common page sizes in points + */ +export const PAGE_SIZES = { + A4: { width: 595.28, height: 841.89 }, + LETTER: { width: 612, height: 792 }, + LEGAL: { width: 612, height: 1008 }, + A3: { width: 841.89, height: 1190.55 }, + A5: { width: 419.53, height: 595.28 }, +}; + +// Re-export react-pdf components for convenience +export { + Document, + Page, + View, + Text, + Image, + Link, + StyleSheet, + Font, + PDFViewer, + BlobProvider, + PDFDownloadLink, +} from '@react-pdf/renderer'; + +// Default export +export default { + renderToBuffer, + createElement, + getFilename, + cmToPoints, + inchesToPoints, + mmToPoints, + PAGE_SIZES, +}; diff --git a/src/core/storage/index.js b/src/core/storage/index.js new file mode 100644 index 0000000..7f5f06e --- /dev/null +++ b/src/core/storage/index.js @@ -0,0 +1,671 @@ +/** + * Zen Storage Module - Cloudflare R2 + * Provides file upload, download, deletion, and management functionality + * Uses native fetch + crypto (AWS Signature V4) — no external dependencies + */ + +import { createHmac, createHash } from 'crypto'; + +// ─── AWS Signature V4 ──────────────────────────────────────────────────────── + +function sha256hex(data) { + return createHash('sha256').update(data).digest('hex'); +} + +function hmac(key, data) { + return createHmac('sha256', Buffer.isBuffer(key) ? key : Buffer.from(key)).update(data).digest(); +} + +function hmacHex(key, data) { + return createHmac('sha256', Buffer.isBuffer(key) ? key : Buffer.from(key)).update(data).digest('hex'); +} + +function amzDate(date) { + return date.toISOString().replace(/[:\-]|\.\d{3}/g, ''); +} + +function dateStamp(date) { + return date.toISOString().slice(0, 10).replace(/-/g, ''); +} + +/** + * Encode a string per AWS Signature V4 spec (encodes everything except A-Z a-z 0-9 - _ . ~) + */ +function encodeS3(str) { + return encodeURIComponent(str) + .replace(/!/g, '%21') + .replace(/'/g, '%27') + .replace(/\(/g, '%28') + .replace(/\)/g, '%29') + .replace(/\*/g, '%2A'); +} + +/** + * Encode a URI path, encoding each segment individually (preserving slashes) + */ +function encodePath(path) { + return path + .split('/') + .map(segment => (segment ? encodeS3(segment) : '')) + .join('/'); +} + +function signingKey(secret, ds, region, service) { + const kDate = hmac('AWS4' + secret, ds); + const kRegion = hmac(kDate, region); + const kService = hmac(kRegion, service); + return hmac(kService, 'aws4_request'); +} + +/** + * Sign an S3 request using AWS Signature V4. + * Returns the full URL and the headers object to pass to fetch. + */ +function signRequest({ method, host, path, query = {}, extraHeaders = {}, bodyBuffer, config, date }) { + const { accessKeyId, secretAccessKey } = config; + const region = 'auto'; + const service = 's3'; + + const ts = amzDate(date); + const ds = dateStamp(date); + const bodyHash = sha256hex(bodyBuffer ?? Buffer.alloc(0)); + + const headers = { + host, + 'x-amz-date': ts, + 'x-amz-content-sha256': bodyHash, + ...extraHeaders, + }; + + const sortedHeaderKeys = Object.keys(headers).sort(); + const canonicalHeaders = sortedHeaderKeys.map(k => `${k}:${headers[k]}\n`).join(''); + const signedHeaders = sortedHeaderKeys.join(';'); + + const canonicalQueryString = Object.keys(query) + .sort() + .map(k => `${encodeS3(k)}=${encodeS3(query[k])}`) + .join('&'); + + const canonicalRequest = [ + method, + encodePath(path), + canonicalQueryString, + canonicalHeaders, + signedHeaders, + bodyHash, + ].join('\n'); + + const scope = `${ds}/${region}/${service}/aws4_request`; + const stringToSign = ['AWS4-HMAC-SHA256', ts, scope, sha256hex(canonicalRequest)].join('\n'); + + const sig = hmacHex(signingKey(secretAccessKey, ds, region, service), stringToSign); + const auth = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${sig}`; + + const requestHeaders = { ...headers, Authorization: auth }; + delete requestHeaders.host; + + const url = canonicalQueryString + ? `https://${host}${path}?${canonicalQueryString}` + : `https://${host}${path}`; + + return { url, headers: requestHeaders }; +} + +/** + * Build a presigned URL (signature embedded in query string, no Authorization header). + * The payload is marked UNSIGNED-PAYLOAD so no body hash is needed at signing time. + */ +function buildPresignedUrl({ method, host, path, expiresIn, config, date }) { + const { accessKeyId, secretAccessKey } = config; + const region = 'auto'; + const service = 's3'; + + const ts = amzDate(date); + const ds = dateStamp(date); + const scope = `${ds}/${region}/${service}/aws4_request`; + + const query = { + 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256', + 'X-Amz-Credential': `${accessKeyId}/${scope}`, + 'X-Amz-Date': ts, + 'X-Amz-Expires': String(expiresIn), + 'X-Amz-SignedHeaders': 'host', + }; + + const canonicalQueryString = Object.keys(query) + .sort() + .map(k => `${encodeS3(k)}=${encodeS3(query[k])}`) + .join('&'); + + const canonicalRequest = [ + method, + encodePath(path), + canonicalQueryString, + `host:${host}\n`, + 'host', + 'UNSIGNED-PAYLOAD', + ].join('\n'); + + const stringToSign = ['AWS4-HMAC-SHA256', ts, scope, sha256hex(canonicalRequest)].join('\n'); + const sig = hmacHex(signingKey(secretAccessKey, ds, region, service), stringToSign); + + return `https://${host}${path}?${canonicalQueryString}&X-Amz-Signature=${sig}`; +} + +// ─── Config ────────────────────────────────────────────────────────────────── + +function getConfig() { + const region = process.env.ZEN_STORAGE_REGION; + const accessKeyId = process.env.ZEN_STORAGE_ACCESS_KEY; + const secretAccessKey = process.env.ZEN_STORAGE_SECRET_KEY; + const bucket = process.env.ZEN_STORAGE_BUCKET; + + if (!region || !accessKeyId || !secretAccessKey) { + throw new Error( + 'Storage credentials are not configured. Please set ZEN_STORAGE_REGION, ZEN_STORAGE_ACCESS_KEY, and ZEN_STORAGE_SECRET_KEY.' + ); + } + if (!bucket) { + throw new Error('ZEN_STORAGE_BUCKET environment variable is not set'); + } + + return { + accessKeyId, + secretAccessKey, + bucket, + host: `${region}.r2.cloudflarestorage.com`, + }; +} + +// ─── Minimal XML helpers ───────────────────────────────────────────────────── + +function xmlFirst(xml, tag) { + const m = xml.match(new RegExp(`<${tag}[^>]*>(.*?)`, 's')); + return m ? m[1] : null; +} + +function xmlAll(xml, tag) { + const re = new RegExp(`<${tag}[^>]*>(.*?)`, 'gs'); + const results = []; + let m; + while ((m = re.exec(xml)) !== null) results.push(m[1]); + return results; +} + +// ─── Body normalizer ───────────────────────────────────────────────────────── + +async function toBuffer(body) { + if (Buffer.isBuffer(body)) return body; + if (body instanceof Uint8Array) return Buffer.from(body); + if (typeof body === 'string') return Buffer.from(body, 'utf8'); + if (body instanceof Blob) return Buffer.from(await body.arrayBuffer()); + return Buffer.from(body); +} + +// ─── Sanitization helpers ───────────────────────────────────────────────────── + +/** + * Strip HTTP header injection characters (\r, \n, \0) from a header value. + * A value containing these characters would break the canonical request format + * and could allow an attacker to inject arbitrary signed headers. + */ +function sanitizeHeaderValue(value) { + return String(value).replace(/[\r\n\0]/g, ''); +} + +/** + * Escape XML special characters to prevent injection into the DeleteObjects payload. + */ +function escapeXml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +// ─── Metadata header helpers ───────────────────────────────────────────────── + +function metaToHeaders(metadata) { + return Object.fromEntries( + Object.entries(metadata).map(([k, v]) => [ + `x-amz-meta-${sanitizeHeaderValue(k).toLowerCase()}`, + sanitizeHeaderValue(v), + ]) + ); +} + +function headersToMeta(headers) { + return Object.fromEntries( + [...headers.entries()] + .filter(([k]) => k.startsWith('x-amz-meta-')) + .map(([k, v]) => [k.replace('x-amz-meta-', ''), v]) + ); +} + +// ─── Storage functions ─────────────────────────────────────────────────────── + +/** + * Upload a file to storage + * @param {Object} options + * @param {string} options.key - File path/key in the bucket + * @param {Buffer|string|Uint8Array|Blob} options.body - File content + * @param {string} options.contentType - MIME type + * @param {Object} options.metadata - Optional metadata key-value pairs + * @param {string} options.cacheControl - Optional cache control header + * @returns {Promise} + */ +async function uploadFile({ key, body, contentType, metadata = {}, cacheControl }) { + try { + const config = getConfig(); + const path = `/${config.bucket}/${key}`; + const date = new Date(); + const bodyBuffer = await toBuffer(body); + + const extraHeaders = { + 'content-type': sanitizeHeaderValue(contentType), + ...metaToHeaders(metadata), + ...(cacheControl && { 'cache-control': sanitizeHeaderValue(cacheControl) }), + }; + + const { url, headers } = signRequest({ + method: 'PUT', + host: config.host, + path, + extraHeaders, + bodyBuffer, + config, + date, + }); + + const response = await fetch(url, { method: 'PUT', headers, body: bodyBuffer }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Upload failed (${response.status}): ${text}`); + } + + return { success: true, data: { key, bucket: config.bucket, contentType }, error: null }; + } catch (error) { + console.error('[ZEN STORAGE] Error uploading file:', error); + return { success: false, data: null, error: error.message }; + } +} + +/** + * Upload an image with optimized settings + * @param {Object} options + * @param {string} options.key - File path/key in the bucket + * @param {Buffer|Blob} options.body - Image content + * @param {string} options.contentType - Image MIME type + * @param {Object} options.metadata - Optional metadata + * @returns {Promise} + */ +async function uploadImage({ key, body, contentType, metadata = {} }) { + return uploadFile({ key, body, contentType, metadata, cacheControl: 'public, max-age=31536000' }); +} + +/** + * Delete a file from storage + * @param {string} key - File path/key to delete + * @returns {Promise} + */ +async function deleteFile(key) { + try { + const config = getConfig(); + const path = `/${config.bucket}/${key}`; + const date = new Date(); + + const { url, headers } = signRequest({ method: 'DELETE', host: config.host, path, config, date }); + const response = await fetch(url, { method: 'DELETE', headers }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Delete failed (${response.status}): ${text}`); + } + + return { success: true, data: { key }, error: null }; + } catch (error) { + console.error('[ZEN STORAGE] Error deleting file:', error); + return { success: false, data: null, error: error.message }; + } +} + +/** + * Delete multiple files from storage + * @param {string[]} keys - Array of file paths/keys to delete + * @returns {Promise} + */ +async function deleteFiles(keys) { + try { + const config = getConfig(); + const path = `/${config.bucket}`; + const date = new Date(); + + const xmlBody = + `` + + keys.map(k => `${escapeXml(k)}`).join('') + + ``; + const bodyBuffer = Buffer.from(xmlBody, 'utf8'); + const contentMd5 = createHash('md5').update(bodyBuffer).digest('base64'); + + const { url, headers } = signRequest({ + method: 'POST', + host: config.host, + path, + query: { delete: '' }, + extraHeaders: { 'content-type': 'application/xml', 'content-md5': contentMd5 }, + bodyBuffer, + config, + date, + }); + + const response = await fetch(url, { method: 'POST', headers, body: bodyBuffer }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Delete failed (${response.status}): ${text}`); + } + + const xml = await response.text(); + const deleted = xmlAll(xml, 'Deleted').map(b => ({ Key: xmlFirst(b, 'Key') })); + const errors = xmlAll(xml, 'Error').map(b => ({ + Key: xmlFirst(b, 'Key'), + Code: xmlFirst(b, 'Code'), + Message: xmlFirst(b, 'Message'), + })); + + return { success: true, data: { deleted, errors }, error: null }; + } catch (error) { + console.error('[ZEN STORAGE] Error deleting files:', error); + return { success: false, data: null, error: error.message }; + } +} + +/** + * Get a file from storage + * @param {string} key - File path/key to retrieve + * @returns {Promise} File data with metadata + */ +async function getFile(key) { + try { + const config = getConfig(); + const path = `/${config.bucket}/${key}`; + const date = new Date(); + + const { url, headers } = signRequest({ method: 'GET', host: config.host, path, config, date }); + const response = await fetch(url, { method: 'GET', headers }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Get failed (${response.status}): ${text}`); + } + + const buffer = Buffer.from(await response.arrayBuffer()); + + return { + success: true, + data: { + key, + body: buffer, + contentType: response.headers.get('content-type'), + contentLength: Number(response.headers.get('content-length')), + lastModified: response.headers.get('last-modified') + ? new Date(response.headers.get('last-modified')) + : null, + metadata: headersToMeta(response.headers), + }, + error: null, + }; + } catch (error) { + console.error('[ZEN STORAGE] Error getting file:', error); + return { success: false, data: null, error: error.message }; + } +} + +/** + * Get file metadata without downloading the file + * @param {string} key - File path/key + * @returns {Promise} File metadata + */ +async function getFileMetadata(key) { + try { + const config = getConfig(); + const path = `/${config.bucket}/${key}`; + const date = new Date(); + + const { url, headers } = signRequest({ method: 'HEAD', host: config.host, path, config, date }); + const response = await fetch(url, { method: 'HEAD', headers }); + + if (!response.ok) { + throw new Error(`Head failed (${response.status})`); + } + + return { + success: true, + data: { + key, + contentType: response.headers.get('content-type'), + contentLength: Number(response.headers.get('content-length')), + lastModified: response.headers.get('last-modified') + ? new Date(response.headers.get('last-modified')) + : null, + metadata: headersToMeta(response.headers), + etag: response.headers.get('etag'), + }, + error: null, + }; + } catch (error) { + console.error('[ZEN STORAGE] Error getting file metadata:', error); + return { success: false, data: null, error: error.message }; + } +} + +/** + * Check if a file exists in storage + * @param {string} key - File path/key to check + * @returns {Promise} + */ +async function fileExists(key) { + const result = await getFileMetadata(key); + return result.success; +} + +/** + * List files in a directory/prefix + * @param {Object} options + * @param {string} options.prefix - Directory prefix (e.g., 'users/123/') + * @param {number} options.maxKeys - Maximum number of keys to return (default: 1000) + * @param {string} options.continuationToken - Token for pagination + * @returns {Promise} + */ +async function listFiles({ prefix = '', maxKeys = 1000, continuationToken } = {}) { + try { + const config = getConfig(); + const path = `/${config.bucket}`; + const date = new Date(); + + // R2/S3 max is 1000 keys per list request + const validMaxKeys = Math.min(Math.max(Math.floor(Number(maxKeys)), 1), 1000); + + const query = { + 'list-type': '2', + 'max-keys': String(validMaxKeys), + ...(prefix && { prefix }), + ...(continuationToken && { 'continuation-token': continuationToken }), + }; + + const { url, headers } = signRequest({ method: 'GET', host: config.host, path, query, config, date }); + const response = await fetch(url, { method: 'GET', headers }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`List failed (${response.status}): ${text}`); + } + + const xml = await response.text(); + const isTruncated = xmlFirst(xml, 'IsTruncated') === 'true'; + const nextContinuationToken = xmlFirst(xml, 'NextContinuationToken'); + const files = xmlAll(xml, 'Contents').map(block => ({ + key: xmlFirst(block, 'Key'), + size: parseInt(xmlFirst(block, 'Size') || '0', 10), + lastModified: xmlFirst(block, 'LastModified') ? new Date(xmlFirst(block, 'LastModified')) : null, + etag: (xmlFirst(block, 'ETag') || '').replace(/"/g, ''), + })); + + return { + success: true, + data: { files, isTruncated, nextContinuationToken, count: files.length }, + error: null, + }; + } catch (error) { + console.error('[ZEN STORAGE] Error listing files:', error); + return { success: false, data: null, error: error.message }; + } +} + +/** + * Generate a presigned URL for temporary access to a file + * @param {Object} options + * @param {string} options.key - File path/key + * @param {number} options.expiresIn - URL expiration time in seconds (default: 3600) + * @param {string} options.operation - 'get' or 'put' (default: 'get') + * @returns {Promise} + */ +async function getPresignedUrl({ key, expiresIn = 3600, operation = 'get' }) { + try { + const config = getConfig(); + const path = `/${config.bucket}/${key}`; + const date = new Date(); + const method = operation === 'put' ? 'PUT' : 'GET'; + + // R2/S3 max presigned URL lifetime is 7 days (604800 seconds) + const validExpiresIn = Math.min(Math.max(Math.floor(Number(expiresIn)), 1), 604800); + if (!Number.isFinite(validExpiresIn)) { + throw new Error('expiresIn must be a finite positive number'); + } + + const url = buildPresignedUrl({ method, host: config.host, path, expiresIn: validExpiresIn, config, date }); + + return { success: true, data: { key, url, expiresIn: validExpiresIn, operation }, error: null }; + } catch (error) { + console.error('[ZEN STORAGE] Error generating presigned URL:', error); + return { success: false, data: null, error: error.message }; + } +} + +/** + * Copy a file within the same bucket + * @param {Object} options + * @param {string} options.sourceKey - Source file path/key + * @param {string} options.destinationKey - Destination file path/key + * @returns {Promise} + */ +async function copyFile({ sourceKey, destinationKey }) { + try { + const getResult = await getFile(sourceKey); + if (!getResult.success) return getResult; + + const uploadResult = await uploadFile({ + key: destinationKey, + body: getResult.data.body, + contentType: getResult.data.contentType, + metadata: getResult.data.metadata, + }); + + if (uploadResult.success) { + console.log(`[ZEN STORAGE] File copied from ${sourceKey} to ${destinationKey}`); + } + + return uploadResult; + } catch (error) { + console.error('[ZEN STORAGE] Error copying file:', error); + return { success: false, data: null, error: error.message }; + } +} + +/** + * Proxy a file from storage, returning a handler-ready response object. + * Use this instead of presigned URLs to avoid exposing storage URLs to clients. + * The returned object is consumed directly by the API router to stream the file. + * @param {string} key - File path/key to retrieve + * @param {Object} options + * @param {string} [options.filename] - Optional download filename (Content-Disposition) + * @returns {Promise} + */ +async function proxyFile(key, { filename } = {}) { + const result = await getFile(key); + if (!result.success) return { success: false, error: result.error }; + return { + success: true, + file: { + body: result.data.body, + contentType: result.data.contentType, + contentLength: result.data.contentLength, + ...(filename && { filename }), + }, + }; +} + +/** + * Move a file (copy + delete source) + * @param {Object} options + * @param {string} options.sourceKey - Source file path/key + * @param {string} options.destinationKey - Destination file path/key + * @returns {Promise} + */ +async function moveFile({ sourceKey, destinationKey }) { + try { + const copyResult = await copyFile({ sourceKey, destinationKey }); + if (!copyResult.success) return copyResult; + + const deleteResult = await deleteFile(sourceKey); + if (!deleteResult.success) { + console.warn( + `[ZEN STORAGE] File copied to ${destinationKey} but failed to delete source ${sourceKey}` + ); + } else { + console.log(`[ZEN STORAGE] File moved from ${sourceKey} to ${destinationKey}`); + } + + return copyResult; + } catch (error) { + console.error('[ZEN STORAGE] Error moving file:', error); + return { success: false, data: null, error: error.message }; + } +} + +// Export utility functions +export { + generateUniqueFilename, + getFileExtension, + getMimeType, + validateFileType, + validateFileSize, + formatFileSize, + generateUserFilePath, + generateOrgFilePath, + generateBlogFilePath, + sanitizeFilename, + validateImageDimensions, + validateUpload, + FILE_TYPE_PRESETS, + FILE_SIZE_LIMITS, +} from './utils.js'; + +// Export storage functions +export { + uploadFile, + uploadImage, + deleteFile, + deleteFiles, + getFile, + getFileMetadata, + fileExists, + listFiles, + getPresignedUrl, + proxyFile, + copyFile, + moveFile, +}; diff --git a/src/core/storage/utils.js b/src/core/storage/utils.js new file mode 100644 index 0000000..74d3bd5 --- /dev/null +++ b/src/core/storage/utils.js @@ -0,0 +1,264 @@ +/** + * Storage utility functions + * Helper functions for file handling, validation, and naming + */ + +import crypto from 'crypto'; + +/** + * Generate a unique filename with timestamp and random hash + * @param {string} originalName - Original filename + * @param {string} prefix - Optional prefix for the filename + * @returns {string} Unique filename + */ +export function generateUniqueFilename(originalName, prefix = '') { + const timestamp = Date.now(); + const randomHash = crypto.randomBytes(8).toString('hex'); + const extension = getFileExtension(originalName); + const basePrefix = prefix ? `${prefix}_` : ''; + return `${basePrefix}${timestamp}_${randomHash}${extension}`; +} + +/** + * Get file extension from filename + * @param {string} filename - Filename + * @returns {string} File extension with dot (e.g., '.jpg') or empty string + */ +export function getFileExtension(filename) { + if (!filename) return ''; + const lastDot = filename.lastIndexOf('.'); + return lastDot === -1 ? '' : filename.substring(lastDot).toLowerCase(); +} + +/** + * Get MIME type from file extension + * @param {string} filename - Filename or extension + * @returns {string} MIME type + */ +export function getMimeType(filename) { + const ext = getFileExtension(filename).toLowerCase(); + + const mimeTypes = { + // Images + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.bmp': 'image/bmp', + + // Documents + '.pdf': 'application/pdf', + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.xls': 'application/vnd.ms-excel', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.ppt': 'application/vnd.ms-powerpoint', + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + '.txt': 'text/plain', + '.csv': 'text/csv', + + // Archives + '.zip': 'application/zip', + '.rar': 'application/x-rar-compressed', + '.7z': 'application/x-7z-compressed', + '.tar': 'application/x-tar', + '.gz': 'application/gzip', + + // Media + '.mp3': 'audio/mpeg', + '.wav': 'audio/wav', + '.mp4': 'video/mp4', + '.avi': 'video/x-msvideo', + '.mov': 'video/quicktime', + '.wmv': 'video/x-ms-wmv', + + // Code + '.js': 'application/javascript', + '.json': 'application/json', + '.xml': 'application/xml', + '.html': 'text/html', + '.css': 'text/css', + }; + + return mimeTypes[ext] || 'application/octet-stream'; +} + +/** + * Validate file type against allowed types + * @param {string} filename - Filename or extension + * @param {string[]} allowedTypes - Array of allowed extensions (e.g., ['.jpg', '.png']) or MIME types + * @returns {boolean} True if file type is allowed + */ +export function validateFileType(filename, allowedTypes) { + if (!allowedTypes || allowedTypes.length === 0) return true; + + const ext = getFileExtension(filename).toLowerCase(); + const mimeType = getMimeType(filename); + + return allowedTypes.some(type => { + if (type.startsWith('.')) { + return ext === type.toLowerCase(); + } + return mimeType === type; + }); +} + +/** + * Validate file size + * @param {number} size - File size in bytes + * @param {number} maxSize - Maximum allowed size in bytes + * @returns {boolean} True if file size is valid + */ +export function validateFileSize(size, maxSize) { + if (!maxSize) return true; + return size <= maxSize; +} + +/** + * Format file size to human-readable format + * @param {number} bytes - Size in bytes + * @param {number} decimals - Number of decimal places (default: 2) + * @returns {string} Formatted size (e.g., '1.5 MB') + */ +export function formatFileSize(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +/** + * Generate a storage path for a user's file + * @param {string|number} userId - User ID + * @param {string} category - File category (e.g., 'profile', 'documents') + * @param {string} filename - Filename + * @returns {string} Storage path (e.g., 'users/123/profile/filename.jpg') + */ +export function generateUserFilePath(userId, category, filename) { + return `users/${userId}/${category}/${filename}`; +} + +/** + * Generate a storage path for organization/tenant files + * @param {string|number} orgId - Organization/tenant ID + * @param {string} category - File category + * @param {string} filename - Filename + * @returns {string} Storage path + */ +export function generateOrgFilePath(orgId, category, filename) { + return `organizations/${orgId}/${category}/${filename}`; +} + +/** + * Generate a storage path for blog post images + * @param {string|number} postIdOrSlug - Post ID or slug (e.g. for temp uploads use timestamp) + * @param {string} filename - Filename + * @returns {string} Storage path (e.g., 'blog/123/filename.jpg') + */ +export function generateBlogFilePath(postIdOrSlug, filename) { + return `blog/${postIdOrSlug}/${filename}`; +} + +/** + * Sanitize filename by removing special characters + * @param {string} filename - Original filename + * @returns {string} Sanitized filename + */ +export function sanitizeFilename(filename) { + const ext = getFileExtension(filename); + const nameWithoutExt = filename.substring(0, filename.length - ext.length); + + // Remove special characters and replace spaces with underscores + const sanitized = nameWithoutExt + .replace(/[^a-zA-Z0-9_-]/g, '_') + .replace(/_{2,}/g, '_') + .replace(/^_+|_+$/g, ''); + + return sanitized + ext; +} + +/** + * Validate image dimensions from buffer + * Note: This is a basic implementation. For production, consider using a library like 'sharp' + * @param {Buffer} buffer - Image buffer + * @param {Object} constraints - Dimension constraints + * @param {number} constraints.maxWidth - Maximum width + * @param {number} constraints.maxHeight - Maximum height + * @param {number} constraints.minWidth - Minimum width + * @param {number} constraints.minHeight - Minimum height + * @returns {Promise} Validation result with dimensions + */ +export async function validateImageDimensions(buffer, constraints = {}) { + // This is a placeholder - in production, use a library like 'sharp' + // For now, we'll return a basic structure + return { + valid: true, + width: null, + height: null, + message: 'Image dimension validation requires additional setup', + }; +} + +/** + * Common file type presets + */ +export const FILE_TYPE_PRESETS = { + IMAGES: ['.jpg', '.jpeg', '.png', '.gif', '.webp'], + IMAGES_NO_GIF: ['.jpg', '.jpeg', '.png', '.webp'], + DOCUMENTS: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.csv'], + PDF_ONLY: ['.pdf'], + VIDEOS: ['.mp4', '.avi', '.mov', '.wmv'], + AUDIO: ['.mp3', '.wav'], + ARCHIVES: ['.zip', '.rar', '.7z', '.tar', '.gz'], +}; + +/** + * Common file size limits (in bytes) + */ +export const FILE_SIZE_LIMITS = { + AVATAR: 5 * 1024 * 1024, // 5 MB + IMAGE: 10 * 1024 * 1024, // 10 MB + DOCUMENT: 50 * 1024 * 1024, // 50 MB + VIDEO: 500 * 1024 * 1024, // 500 MB + LARGE_FILE: 1024 * 1024 * 1024, // 1 GB +}; + +/** + * Validate upload file + * @param {Object} options - Validation options + * @param {string} options.filename - Filename + * @param {number} options.size - File size in bytes + * @param {string[]} options.allowedTypes - Allowed file types + * @param {number} options.maxSize - Maximum file size + * @returns {Object} Validation result + */ +export function validateUpload({ filename, size, allowedTypes, maxSize }) { + const errors = []; + + if (!filename) { + errors.push('Filename is required'); + } + + if (allowedTypes && !validateFileType(filename, allowedTypes)) { + const typesList = allowedTypes.join(', '); + errors.push(`File type not allowed. Allowed types: ${typesList}`); + } + + if (maxSize && !validateFileSize(size, maxSize)) { + errors.push(`File size exceeds limit of ${formatFileSize(maxSize)}`); + } + + return { + valid: errors.length === 0, + errors, + }; +} + diff --git a/src/core/toast/Toast.js b/src/core/toast/Toast.js new file mode 100644 index 0000000..9e50484 --- /dev/null +++ b/src/core/toast/Toast.js @@ -0,0 +1,133 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { + Tick02Icon, + Cancel01Icon, + AlertCircleIcon, + InformationCircleIcon, + CancelCircleIcon +} from '../../shared/Icons.js'; + +const Toast = ({ + id, + type = 'info', + message, + title, + duration = 5000, + dismissible = true, + isAutoRemoving = false, + onDismiss +}) => { + const [isVisible, setIsVisible] = useState(false); + const [isLeaving, setIsLeaving] = useState(false); + + useEffect(() => { + // Trigger entrance animation + const timer = setTimeout(() => setIsVisible(true), 10); + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + // Trigger exit animation on auto-remove + if (isAutoRemoving && !isLeaving) { + setIsLeaving(true); + } + }, [isAutoRemoving, isLeaving]); + + const handleDismiss = () => { + if (!dismissible) return; + + setIsLeaving(true); + setTimeout(() => { + onDismiss(id); + }, 300); // Match animation duration + }; + + const getIcon = () => { + switch (type) { + case 'success': + return ; + case 'error': + return ; + case 'warning': + return ; + case 'info': + default: + return ; + } + }; + + const getStyles = () => { + const base = 'backdrop-blur-sm shadow-lg transition-colors duration-200'; + const shadow = 'shadow-neutral-900/10 dark:shadow-black/20'; + switch (type) { + case 'success': + return `${base} ${shadow} bg-green-50 border border-green-200 text-green-700 hover:bg-green-100/80 dark:bg-green-500/10 dark:border-green-500/20 dark:text-green-400 dark:hover:bg-green-500/15`; + case 'error': + return `${base} ${shadow} bg-red-50 border border-red-200 text-red-700 hover:bg-red-100/80 dark:bg-red-500/10 dark:border-red-500/20 dark:text-red-400 dark:hover:bg-red-500/15`; + case 'warning': + return `${base} ${shadow} bg-yellow-50 border border-yellow-200 text-yellow-800 hover:bg-yellow-100/80 dark:bg-yellow-500/10 dark:border-yellow-500/20 dark:text-yellow-400 dark:hover:bg-yellow-500/15`; + case 'info': + default: + return `${base} ${shadow} bg-blue-50 border border-blue-200 text-blue-700 hover:bg-blue-100/80 dark:bg-blue-500/10 dark:border-blue-500/20 dark:text-blue-400 dark:hover:bg-blue-500/15`; + } + }; + + const getTitle = () => { + if (title) return title; + switch (type) { + case 'success': + return 'Success'; + case 'error': + return 'Error'; + case 'warning': + return 'Warning'; + case 'info': + default: + return 'Information'; + } + }; + + return ( +
+ {/* Icon */} +
+ {getIcon()} +
+ + {/* Content */} +
+

{getTitle()}

+

{message}

+
+ + {/* Dismiss button */} + {dismissible && ( + + )} +
+ ); +}; + +export default Toast; + diff --git a/src/core/toast/ToastContainer.js b/src/core/toast/ToastContainer.js new file mode 100644 index 0000000..ceee287 --- /dev/null +++ b/src/core/toast/ToastContainer.js @@ -0,0 +1,132 @@ +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; +import { useToast } from './ToastContext'; +import Toast from './Toast'; + +const ToastContainer = ({ maxToasts = 5 }) => { + const { toasts, removeToast } = useToast(); + const [isHovered, setIsHovered] = useState(false); + const [toastHeights, setToastHeights] = useState({}); + const hoverTimeoutRef = useRef(null); + const toastRefs = useRef({}); + + // Limit the number of visible toasts + const visibleToasts = toasts.slice(-maxToasts); + + // Measure toast heights + useEffect(() => { + const newHeights = {}; + visibleToasts.forEach((toast) => { + const element = toastRefs.current[toast.id]; + if (element) { + newHeights[toast.id] = element.offsetHeight; + } + }); + + // Update only if heights have changed + const hasChanged = visibleToasts.some(toast => + newHeights[toast.id] !== toastHeights[toast.id] + ); + + if (hasChanged) { + setToastHeights(newHeights); + } + }, [visibleToasts, toastHeights]); + + const handleMouseEnter = (isLastToast) => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + + // Trigger hover only on most recent toast OR if already in hover mode + if (isLastToast || isHovered) { + setIsHovered(true); + } + }; + + const handleMouseLeave = () => { + // Delay before closing to avoid flickering + hoverTimeoutRef.current = setTimeout(() => { + setIsHovered(false); + }, 150); + }; + + // Calculate position of each toast based on actual heights + const calculatePosition = (index) => { + const isRecent = index === visibleToasts.length - 1; + const distanceFromRecent = visibleToasts.length - 1 - index; + + if (isRecent) { + return 0; // Most recent stays at 0 + } + + if (isHovered) { + // In hover mode: add heights of more recent toasts + margin + let totalOffset = 0; + for (let i = index + 1; i < visibleToasts.length; i++) { + const toastId = visibleToasts[i].id; + const height = toastHeights[toastId] || 60; // default height + totalOffset += height + 10; // height + 10px margin + } + return -totalOffset; + } else { + // In stack mode: reduced spacing based on height + const recentToastHeight = toastHeights[visibleToasts[visibleToasts.length - 1].id] || 60; + return -(distanceFromRecent * Math.min(recentToastHeight * 0.15, 12)); // Maximum 12px per level + } + }; + + if (visibleToasts.length === 0) { + return null; + } + + return ( +
+
+ {visibleToasts.map((toast, index) => { + // The last toast (index length-1) is always the most recent + const isRecent = index === visibleToasts.length - 1; + const distanceFromRecent = visibleToasts.length - 1 - index; + + // Calculations for both modes + const scale = isHovered ? 1 : (isRecent ? 1 : Math.max(0.7, 1 - (distanceFromRecent * 0.08))); + const translateY = calculatePosition(index); + + return ( +
toastRefs.current[toast.id] = el} + className={` + absolute bottom-0 right-0 + pointer-events-auto + transition-all duration-500 ease-out + `} + style={{ + transform: `scale(${scale}) translateY(${translateY}px)`, + zIndex: isRecent ? 10 : (10 - distanceFromRecent), + }} + onMouseEnter={() => handleMouseEnter(isRecent)} + onMouseLeave={handleMouseLeave} + > + +
+ ); + })} +
+
+ ); +}; + +export default ToastContainer; + diff --git a/src/core/toast/ToastContext.js b/src/core/toast/ToastContext.js new file mode 100644 index 0000000..7918451 --- /dev/null +++ b/src/core/toast/ToastContext.js @@ -0,0 +1,110 @@ +'use client'; + +import React, { createContext, useContext, useState, useCallback } from 'react'; + +const ToastContext = createContext(); + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within a ToastProvider'); + } + return context; +}; + +export const ToastProvider = ({ children }) => { + const [toasts, setToasts] = useState([]); + + const addToast = useCallback((toast) => { + const id = Date.now() + Math.random(); + const newToast = { + id, + type: 'info', + duration: 5000, + dismissible: true, + isAutoRemoving: false, + ...toast, + }; + + setToasts(prev => [...prev, newToast]); + + if (newToast.duration > 0) { + setTimeout(() => { + // First mark the toast for auto-removal + setToasts(prev => prev.map(t => + t.id === id ? { ...t, isAutoRemoving: true } : t + )); + + // Then remove it after animation (300ms) + setTimeout(() => { + removeToast(id); + }, 300); + }, newToast.duration); + } + + return id; + }, []); + + const removeToast = useCallback((id) => { + setToasts(prev => prev.filter(toast => toast.id !== id)); + }, []); + + const clearAllToasts = useCallback(() => { + setToasts([]); + }, []); + + // Convenience methods for different toast types + const success = useCallback((message, options = {}) => { + return addToast({ + type: 'success', + message, + ...options, + }); + }, [addToast]); + + const error = useCallback((message, options = {}) => { + return addToast({ + type: 'error', + message, + duration: 7000, // Longer duration for errors + ...options, + }); + }, [addToast]); + + const warning = useCallback((message, options = {}) => { + return addToast({ + type: 'warning', + message, + duration: 6000, + ...options, + }); + }, [addToast]); + + const info = useCallback((message, options = {}) => { + return addToast({ + type: 'info', + message, + ...options, + }); + }, [addToast]); + + const value = { + toasts, + addToast, + removeToast, + clearAllToasts, + success, + error, + warning, + info, + }; + + return ( + + {children} + + ); +}; + +export default ToastContext; + diff --git a/src/core/toast/index.js b/src/core/toast/index.js new file mode 100644 index 0000000..255ddc7 --- /dev/null +++ b/src/core/toast/index.js @@ -0,0 +1,6 @@ +'use client'; + +export { default as Toast } from './Toast'; +export { ToastProvider, useToast } from './ToastContext'; +export { default as ToastContainer } from './ToastContainer'; + diff --git a/src/features/admin/actions.js b/src/features/admin/actions.js new file mode 100644 index 0000000..a263877 --- /dev/null +++ b/src/features/admin/actions.js @@ -0,0 +1,12 @@ +/** + * Admin Server Actions + * + * These are exported separately from admin/index.js to avoid bundling + * server-side code (which includes database imports) into client components. + * + * Usage: + * import { getDashboardStats, getModuleDashboardStats } from '@hykocx/zen/admin/actions'; + */ + +export { getDashboardStats } from './actions/statsActions.js'; +export { getAllModuleDashboardStats as getModuleDashboardStats } from '@hykocx/zen/modules/actions'; diff --git a/src/features/admin/actions/statsActions.js b/src/features/admin/actions/statsActions.js new file mode 100644 index 0000000..318f43c --- /dev/null +++ b/src/features/admin/actions/statsActions.js @@ -0,0 +1,79 @@ +/** + * Admin Stats Actions + * Server-side actions for core dashboard statistics + * + * Module-specific stats are handled by each module's dashboard actions. + * See src/modules/{module}/dashboard/statsActions.js + * + * Usage in your Next.js app: + * + * ```javascript + * // app/(admin)/admin/[...admin]/page.js + * import { protectAdmin } from '@hykocx/zen/admin'; + * import { getDashboardStats, getModuleDashboardStats } from '@hykocx/zen/admin/actions'; + * import { AdminPagesClient } from '@hykocx/zen/admin/pages'; + * + * export default async function AdminPage({ params }) { + * const { user } = await protectAdmin(); + * + * // Fetch core dashboard stats + * const statsResult = await getDashboardStats(); + * const dashboardStats = statsResult.success ? statsResult.stats : null; + * + * // Fetch module dashboard stats (for dynamic widgets) + * const moduleStats = await getModuleDashboardStats(); + * + * return ( + * + * ); + * } + * ``` + */ + +'use server'; + +import { query } from '@hykocx/zen/database'; + +/** + * Get total number of users + * @returns {Promise} + */ +async function getTotalUsersCount() { + try { + const result = await query( + `SELECT COUNT(*) as count FROM zen_auth_users` + ); + return parseInt(result.rows[0].count) || 0; + } catch (error) { + console.error('Error getting users count:', error); + return 0; + } +} + +/** + * Get core dashboard statistics + * @returns {Promise} + */ +export async function getDashboardStats() { + try { + const totalUsers = await getTotalUsersCount(); + + return { + success: true, + stats: { + totalUsers, + } + }; + } catch (error) { + console.error('Error getting dashboard stats:', error); + return { + success: false, + error: error.message || 'Failed to get dashboard statistics' + }; + } +} diff --git a/src/features/admin/components/AdminHeader.js b/src/features/admin/components/AdminHeader.js new file mode 100644 index 0000000..625c556 --- /dev/null +++ b/src/features/admin/components/AdminHeader.js @@ -0,0 +1,214 @@ +'use client'; + +import React from 'react'; +import { Menu, Transition } from '@headlessui/react'; +import { Fragment } from 'react'; +import { ChevronDownIcon } from '../../../shared/Icons.js'; +import { useRouter } from 'next/navigation'; +import ThemeToggle from './ThemeToggle'; + +const AdminHeader = ({ isMobileMenuOpen, setIsMobileMenuOpen, user, onLogout, appName = 'ZEN' }) => { + const router = useRouter(); + + const getImageUrl = (imageKey) => { + if (!imageKey) return null; + return `/zen/api/storage/${imageKey}`; + }; + + const handleLogout = async () => { + try { + if (onLogout) { + const result = await onLogout(); + if (result && result.success) { + router.push('/auth/login'); + } else { + console.error('Logout failed:', result?.error); + router.push('/auth/login'); + } + } else { + router.push('/auth/login'); + } + } catch (error) { + console.error('Logout error:', error); + router.push('/auth/login'); + } + }; + + const getUserInitials = (name) => { + if (!name) return 'U'; + return name + .split(' ') + .map(n => n[0]) + .join('') + .toUpperCase() + .slice(0, 2); + }; + + const quickLinks = []; + + const userInitials = getUserInitials(user?.name); + + return ( +
+
+ {/* Left section - Mobile menu button + Logo (hidden on desktop) */} +
+ +

{appName}

+
+ + {/* Right Section - Theme Toggle + Quick Links + Profile */} +
+ {/* Quick Links - Hidden on very small screens */} + + + {/* Theme Toggle */} + + + {/* User Profile Menu */} + + + {/* Avatar for desktop - hidden on mobile */} +
+ {getImageUrl(user?.image) && ( + {user?.name + )} +
+

{user?.name || 'User'}

+
+
+ {/* Avatar for mobile - visible on mobile only */} +
+ {getImageUrl(user?.image) && ( + {user?.name + )} +
+ +
+ + + +
+
+ {getImageUrl(user?.image) && ( + {user?.name + )} +
+

{user?.name || 'User'}

+

{user?.email || 'email@example.com'}

+
+
+
+ + {/* Quick Links for mobile */} + {quickLinks.length > 0 && ( +
+ {quickLinks.map((link) => ( + + {({ active }) => ( + + + + + {link.name} + + )} + + ))} +
+ )} + +
+ + {({ active }) => ( + + + + + Mon profil + + )} + + +
+ + {({ active }) => ( + + )} + +
+
+
+
+
+
+
+
+ ); +}; + +export default AdminHeader; diff --git a/src/features/admin/components/AdminPages.js b/src/features/admin/components/AdminPages.js new file mode 100644 index 0000000..186f5db --- /dev/null +++ b/src/features/admin/components/AdminPages.js @@ -0,0 +1,85 @@ +'use client'; + +/** + * Admin Pages Component + * + * This component handles both core admin pages and module pages. + * Module pages are loaded dynamically on the client where hooks work properly. + */ + +import { Suspense } from 'react'; +import DashboardPage from './pages/DashboardPage.js'; +import UsersPage from './pages/UsersPage.js'; +import UserEditPage from './pages/UserEditPage.js'; +import ProfilePage from './pages/ProfilePage.js'; +import { getModulePageLoader } from '../../../modules/modules.pages.js'; + +// Loading component for suspense +function PageLoading() { + return ( +
+
+
+ ); +} + +export default function AdminPagesClient({ + params, + user, + dashboardStats = null, + moduleStats = {}, + modulePageInfo = null, + routeInfo = null, + enabledModules = {} +}) { + // If this is a module page, render it with lazy loading + if (modulePageInfo && routeInfo) { + const LazyComponent = getModulePageLoader(modulePageInfo.module, modulePageInfo.path); + if (LazyComponent) { + // Build props for the page + const pageProps = { user }; + if (routeInfo.action === 'edit' && routeInfo.id) { + // Add ID props for edit pages (modules may use different prop names) + pageProps.id = routeInfo.id; + pageProps.invoiceId = routeInfo.id; + pageProps.clientId = routeInfo.id; + pageProps.itemId = routeInfo.id; + pageProps.categoryId = routeInfo.id; + pageProps.transactionId = routeInfo.id; + pageProps.recurrenceId = routeInfo.id; + pageProps.templateId = routeInfo.id; + pageProps.postId = routeInfo.id; + } + + return ( + }> + + + ); + } + } + + // Determine core page from routeInfo or params + let currentPage = 'dashboard'; + if (routeInfo?.path) { + const parts = routeInfo.path.split('/').filter(Boolean); + currentPage = parts[1] || 'dashboard'; // /admin/[page] + } else if (params?.admin) { + currentPage = params.admin[0] || 'dashboard'; + } + + // Core page components mapping (non-module pages) + const usersPageComponent = routeInfo?.action === 'edit' && routeInfo?.id + ? () => + : () => ; + + const corePages = { + dashboard: () => , + users: usersPageComponent, + profile: () => , + }; + + // Render the appropriate core page or default to dashboard + const CorePageComponent = corePages[currentPage]; + return CorePageComponent ? : ; +} diff --git a/src/features/admin/components/AdminPagesLayout.js b/src/features/admin/components/AdminPagesLayout.js new file mode 100644 index 0000000..301ea06 --- /dev/null +++ b/src/features/admin/components/AdminPagesLayout.js @@ -0,0 +1,29 @@ +'use client'; + +import AdminSidebar from './AdminSidebar'; +import { useState } from 'react'; +import AdminHeader from './AdminHeader'; + +export default function AdminPagesLayout({ children, user, onLogout, appName, enabledModules, navigationSections }) { + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + + return ( +
+ +
+ +
+
+ {children} +
+
+
+
+ ); +} diff --git a/src/features/admin/components/AdminSidebar.js b/src/features/admin/components/AdminSidebar.js new file mode 100644 index 0000000..a6d69fa --- /dev/null +++ b/src/features/admin/components/AdminSidebar.js @@ -0,0 +1,234 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import * as Icons from '../../../shared/Icons.js'; +import { ChevronDownIcon } from '../../../shared/Icons.js'; + +/** + * Resolve icon name (string) to icon component + * Icons are passed as strings from server to avoid serialization issues + */ +function resolveIcon(iconNameOrComponent) { + // If it's already a component (function), return it + if (typeof iconNameOrComponent === 'function') { + return iconNameOrComponent; + } + // If it's a string, look up in Icons + if (typeof iconNameOrComponent === 'string') { + return Icons[iconNameOrComponent] || Icons.DashboardSquare03Icon; + } + // Default fallback + return Icons.DashboardSquare03Icon; +} + +const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledModules, navigationSections: serverNavigationSections }) => { + const pathname = usePathname(); + + // State to manage collapsed sections (all open by default) + const [collapsedSections, setCollapsedSections] = useState(new Set()); + + // Function to toggle a section's state + const toggleSection = (sectionId) => { + // Find the section to check if it has active items + const section = navigationSections.find(s => s.id === sectionId); + + // Don't allow collapsing sections with active items + if (section && isSectionActive(section)) { + return; + } + + setCollapsedSections(prev => { + const newCollapsed = new Set(prev); + if (newCollapsed.has(sectionId)) { + newCollapsed.delete(sectionId); + } else { + newCollapsed.add(sectionId); + } + return newCollapsed; + }); + }; + + // Handle mobile menu closure when clicking on a link + const handleMobileLinkClick = () => { + setIsMobileMenuOpen(false); + }; + + // Close mobile menu on screen size change + useEffect(() => { + const handleResize = () => { + if (window.innerWidth >= 1024) { // lg breakpoint + setIsMobileMenuOpen(false); + } + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [setIsMobileMenuOpen]); + + // Function to check if any item in a section is currently active + const isSectionActive = (section) => { + return section.items.some(item => item.current); + }; + + // Function to check if a section should be rendered as a direct link + const shouldRenderAsDirectLink = (section) => { + // Check if there's only one item and it has the same name as the section + return section.items.length === 1 && + section.items[0].name.toLowerCase() === section.title.toLowerCase(); + }; + + // Update collapsed sections when pathname changes to ensure active sections are open + useEffect(() => { + setCollapsedSections(prev => { + const newSet = new Set(prev); + // Add any sections that have active items to ensure they stay open + navigationSections.forEach(section => { + if (isSectionActive(section)) { + newSet.add(section.id); + } + }); + return newSet; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pathname]); + + // Use server-provided navigation sections if available, otherwise use core-only fallback + // Server navigation includes module navigation, fallback only has core pages + // Update the 'current' property based on the actual pathname (client-side) + const navigationSections = serverNavigationSections.map(section => ({ + ...section, + items: section.items.map(item => ({ + ...item, + current: pathname === item.href || pathname.startsWith(item.href + '/') + })) + })); + + // Function to render a complete navigation section + const renderNavSection = (section) => { + const Icon = resolveIcon(section.icon); + + // If section should be rendered as a direct link + if (shouldRenderAsDirectLink(section)) { + const item = section.items[0]; + return ( +
+ +
+ + {section.title} +
+ {item.badge && ( + + {item.badge} + + )} + +
+ ); + } + + // Regular section with expandable sub-items + const isCollapsed = !collapsedSections.has(section.id); + + return ( +
+ +
+
    + {section.items.map(renderNavItem)} +
+
+
+ ); + }; + + // Function to render a navigation item + const renderNavItem = (item) => { + const Icon = resolveIcon(item.icon); + return ( +
  • + +
    + + {item.name} +
    + {item.badge && ( + + {item.badge} + + )} + +
  • + ); + }; + + return ( + <> + {/* Mobile overlay */} + {isMobileMenuOpen && ( +
    setIsMobileMenuOpen(false)} + /> + )} + + {/* Sidebar */} +
    + {/* Logo Section */} + +

    {appName}

    + + Admin + + + + {/* Navigation */} + +
    + + ); +}; + +export default AdminSidebar; diff --git a/src/features/admin/components/ThemeToggle.js b/src/features/admin/components/ThemeToggle.js new file mode 100644 index 0000000..c2a0da7 --- /dev/null +++ b/src/features/admin/components/ThemeToggle.js @@ -0,0 +1,83 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Sun01Icon, Moon02Icon, SunCloud02Icon, MoonCloudIcon } from '../../../shared/Icons.js'; + +function getNextTheme(current) { + const systemIsDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + if (current === 'auto') return systemIsDark ? 'light' : 'dark'; + if (current === 'dark') return systemIsDark ? 'auto' : 'light'; + return systemIsDark ? 'dark' : 'auto'; +} + +function getAutoIcon(systemIsDark) { + return systemIsDark ? MoonCloudIcon : SunCloud02Icon; +} + +const THEME_ICONS = { + light: Sun01Icon, + dark: Moon02Icon, +}; + +function getStoredTheme() { + const stored = localStorage.getItem('theme'); + if (stored === 'light' || stored === 'dark') return stored; + return 'auto'; +} + +function applyTheme(theme) { + if (theme === 'dark') { + document.documentElement.classList.add('dark'); + localStorage.setItem('theme', 'dark'); + } else if (theme === 'light') { + document.documentElement.classList.remove('dark'); + localStorage.setItem('theme', 'light'); + } else { + localStorage.removeItem('theme'); + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + document.documentElement.classList.toggle('dark', prefersDark); + } +} + +function useTheme() { + const [theme, setTheme] = useState('auto'); + const [systemIsDark, setSystemIsDark] = useState(false); + + useEffect(() => { + setTheme(getStoredTheme()); + setSystemIsDark(window.matchMedia('(prefers-color-scheme: dark)').matches); + + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + function onSystemChange(e) { + setSystemIsDark(e.matches); + if (localStorage.getItem('theme')) return; + document.documentElement.classList.toggle('dark', e.matches); + } + mq.addEventListener('change', onSystemChange); + return () => mq.removeEventListener('change', onSystemChange); + }, []); + + function toggle() { + const next = getNextTheme(theme); + setTheme(next); + applyTheme(next); + } + + return { theme, toggle, systemIsDark }; +} + +export default function ThemeToggle() { + const { theme, toggle, systemIsDark } = useTheme(); + const Icon = theme === 'auto' ? getAutoIcon(systemIsDark) : THEME_ICONS[theme]; + + return ( + + ); +} diff --git a/src/features/admin/components/index.js b/src/features/admin/components/index.js new file mode 100644 index 0000000..b043e75 --- /dev/null +++ b/src/features/admin/components/index.js @@ -0,0 +1,6 @@ +/** + * Admin Components Exports + */ + +export { default as AdminPagesClient } from './AdminPages.js'; +export { default as AdminPagesLayout } from './AdminPagesLayout.js'; diff --git a/src/features/admin/components/pages/DashboardPage.js b/src/features/admin/components/pages/DashboardPage.js new file mode 100644 index 0000000..f93e0f4 --- /dev/null +++ b/src/features/admin/components/pages/DashboardPage.js @@ -0,0 +1,64 @@ +'use client'; + +/** + * Admin Dashboard Page + * Displays core stats and dynamically loads module dashboard widgets + */ + +import { Suspense } from 'react'; +import { StatCard } from '../../../../shared/components'; +import { UserMultiple02Icon } from '../../../../shared/Icons.js'; +import { getModuleDashboardWidgets } from '../../../../modules/modules.pages.js'; + +/** + * Loading placeholder for widgets + */ +function WidgetLoading() { + return ( +
    + ); +} + +export default function DashboardPage({ user, stats, moduleStats = {}, enabledModules = {} }) { + const loading = !stats; + + // Get only enabled module dashboard widgets + const allModuleWidgets = getModuleDashboardWidgets(); + const moduleWidgets = Object.fromEntries( + Object.entries(allModuleWidgets).filter(([moduleName]) => enabledModules[moduleName]) + ); + + return ( +
    +
    +
    +

    + Tableau de bord +

    +

    Vue d'ensemble de votre application

    +
    +
    + +
    + {/* Module dashboard widgets (dynamically loaded) */} + {Object.entries(moduleWidgets).map(([moduleName, widgets]) => ( + widgets.map((Widget, index) => ( + }> + + + )) + ))} + + {/* Core stats - always shown */} + +
    +
    + ); +} diff --git a/src/features/admin/components/pages/ProfilePage.js b/src/features/admin/components/pages/ProfilePage.js new file mode 100644 index 0000000..942ba0c --- /dev/null +++ b/src/features/admin/components/pages/ProfilePage.js @@ -0,0 +1,331 @@ +'use client'; + +import React, { useState, useEffect, useRef } from 'react'; +import { Card, Input, Button } from '../../../../shared/components'; +import { useToast } from '@hykocx/zen/toast'; + +const ProfilePage = ({ user: initialUser }) => { + const toast = useToast(); + const [user, setUser] = useState(initialUser); + const [loading, setLoading] = useState(false); + const [uploadingImage, setUploadingImage] = useState(false); + const [imagePreview, setImagePreview] = useState(null); + const fileInputRef = useRef(null); + const [formData, setFormData] = useState({ + name: initialUser?.name || '' + }); + + // Helper function to get image URL from storage key + const getImageUrl = (imageKey) => { + if (!imageKey) return null; + return `/zen/api/storage/${imageKey}`; + }; + + useEffect(() => { + if (initialUser) { + setFormData({ + name: initialUser.name || '' + }); + setImagePreview(getImageUrl(initialUser.image)); + } + }, [initialUser]); + + const handleChange = (value) => { + setFormData(prev => ({ + ...prev, + name: value + })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!formData.name.trim()) { + toast.error('Le nom est requis'); + return; + } + + setLoading(true); + + try { + const response = await fetch('/zen/api/users/profile', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + name: formData.name.trim() + }) + }); + + const data = await response.json(); + + if (!response.ok || !data.success) { + throw new Error(data.error || 'Échec de la mise à jour du profil'); + } + + setUser(data.user); + toast.success('Profil mis à jour avec succès'); + + // Refresh the page to update the user data in the header + setTimeout(() => { + window.location.reload(); + }, 1000); + } catch (error) { + console.error('Error updating profile:', error); + toast.error(error.message || 'Échec de la mise à jour du profil'); + } finally { + setLoading(false); + } + }; + + const handleReset = () => { + setFormData({ + name: user?.name || '' + }); + }; + + const handleImageSelect = async (e) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Validate file type + if (!file.type.startsWith('image/')) { + toast.error('Veuillez sélectionner un fichier image'); + return; + } + + // Validate file size (5MB) + if (file.size > 5 * 1024 * 1024) { + toast.error("L'image doit faire moins de 5MB"); + return; + } + + // Show preview + const reader = new FileReader(); + reader.onloadend = () => { + setImagePreview(reader.result); + }; + reader.readAsDataURL(file); + + // Upload image + setUploadingImage(true); + try { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch('/zen/api/users/profile/picture', { + method: 'POST', + credentials: 'include', + body: formData + }); + + const data = await response.json(); + + if (!response.ok || !data.success) { + throw new Error(data.message || 'Échec du téléchargement de l\'image'); + } + + setUser(data.user); + setImagePreview(getImageUrl(data.user.image)); + toast.success('Photo de profil mise à jour avec succès'); + } catch (error) { + console.error('Error uploading image:', error); + toast.error(error.message || 'Échec du téléchargement de l\'image'); + // Revert preview on error + setImagePreview(getImageUrl(user?.image)); + } finally { + setUploadingImage(false); + } + }; + + const handleRemoveImage = async () => { + if (!user?.image) return; + + setUploadingImage(true); + try { + const response = await fetch('/zen/api/users/profile/picture', { + method: 'DELETE', + credentials: 'include' + }); + + const data = await response.json(); + + if (!response.ok || !data.success) { + throw new Error(data.message || 'Échec de la suppression de l\'image'); + } + + setUser(data.user); + setImagePreview(null); + toast.success('Photo de profil supprimée avec succès'); + } catch (error) { + console.error('Error removing image:', error); + toast.error(error.message || 'Échec de la suppression de l\'image'); + } finally { + setUploadingImage(false); + } + }; + + const getUserInitials = (name) => { + if (!name) return 'U'; + return name + .split(' ') + .map(n => n[0]) + .join('') + .toUpperCase() + .slice(0, 2); + }; + + const hasChanges = formData.name !== user?.name; + + return ( +
    + {/* Header */} +
    +
    +

    + Mon profil +

    +

    + Gérez les informations de votre compte +

    +
    +
    + + {/* Content */} +
    + +
    +

    + Photo de profil +

    +
    +
    + {imagePreview ? ( + Profile + ) : ( +
    + + {getUserInitials(user?.name)} + +
    + )} + {uploadingImage && ( +
    +
    +
    + )} +
    +
    +

    + Téléchargez une nouvelle photo de profil. Taille max 5MB. +

    +
    + + + {imagePreview && ( + + )} +
    +
    +
    +
    +
    + + +
    +
    +

    + Informations personnelles +

    + +
    + + + +
    + +
    + +
    +
    + + {/* Action Buttons */} +
    + + +
    +
    +
    +
    +
    + ); +}; + +export default ProfilePage; diff --git a/src/features/admin/components/pages/UserEditPage.js b/src/features/admin/components/pages/UserEditPage.js new file mode 100644 index 0000000..3677f35 --- /dev/null +++ b/src/features/admin/components/pages/UserEditPage.js @@ -0,0 +1,254 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { Button, Card, Input, Select, Loading } from '../../../../shared/components'; +import { useToast } from '@hykocx/zen/toast'; + +/** + * User Edit Page Component + * Page for editing an existing user (admin only) + */ +const UserEditPage = ({ userId, user, enabledModules = {} }) => { + const router = useRouter(); + const toast = useToast(); + const clientsModuleActive = Boolean(enabledModules?.clients); + + const [userData, setUserData] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [clients, setClients] = useState([]); + const [formData, setFormData] = useState({ + name: '', + role: 'user', + email_verified: 'false', + client_id: '' + }); + const [errors, setErrors] = useState({}); + + const roleOptions = [ + { value: 'user', label: 'Utilisateur' }, + { value: 'admin', label: 'Admin' } + ]; + + const emailVerifiedOptions = [ + { value: 'false', label: 'Non vérifié' }, + { value: 'true', label: 'Vérifié' } + ]; + + useEffect(() => { + loadUser(); + }, [userId]); + + useEffect(() => { + if (clientsModuleActive) { + fetch('/zen/api/admin/clients?limit=500', { credentials: 'include' }) + .then(res => res.json()) + .then(data => data.clients ? setClients(data.clients) : setClients([])) + .catch(() => setClients([])); + } + }, [clientsModuleActive]); + + const loadUser = async () => { + try { + setLoading(true); + const response = await fetch(`/zen/api/users/${userId}`, { + credentials: 'include' + }); + const data = await response.json(); + + if (data.user) { + setUserData(data.user); + setFormData(prev => ({ + ...prev, + name: data.user.name || '', + role: data.user.role || 'user', + email_verified: data.user.email_verified ? 'true' : 'false', + client_id: data.linkedClient ? String(data.linkedClient.id) : '' + })); + } else { + toast.error(data.message || 'Utilisateur introuvable'); + } + } catch (error) { + console.error('Error loading user:', error); + toast.error('Impossible de charger l\'utilisateur'); + } finally { + setLoading(false); + } + }; + + const handleInputChange = (field, value) => { + setFormData(prev => ({ ...prev, [field]: value })); + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: null })); + } + }; + + const validateForm = () => { + const newErrors = {}; + if (!formData.name || !formData.name.trim()) { + newErrors.name = 'Le nom est requis'; + } + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!validateForm()) return; + + try { + setSaving(true); + const response = await fetch(`/zen/api/users/${userId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + name: formData.name.trim(), + role: formData.role, + email_verified: formData.email_verified === 'true', + ...(clientsModuleActive && { client_id: formData.client_id ? parseInt(formData.client_id, 10) : null }) + }) + }); + const data = await response.json(); + + if (data.success) { + toast.success('Utilisateur mis à jour avec succès'); + router.push('/admin/users'); + } else { + toast.error(data.message || data.error || 'Impossible de mettre à jour l\'utilisateur'); + } + } catch (error) { + console.error('Error updating user:', error); + toast.error('Impossible de mettre à jour l\'utilisateur'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
    + +
    + ); + } + + if (!userData) { + return ( +
    +
    +
    +

    Modifier l'utilisateur

    +

    Utilisateur introuvable

    +
    + +
    + +
    +

    Utilisateur introuvable

    +

    L'utilisateur que vous recherchez n'existe pas ou a été supprimé.

    +
    +
    +
    + ); + } + + return ( +
    +
    +
    +

    Modifier l'utilisateur

    +

    {userData.email}

    +
    + +
    + +
    + +
    +

    Informations de l'utilisateur

    + +
    + handleInputChange('name', value)} + placeholder="Nom de l'utilisateur" + error={errors.name} + /> + + + + handleInputChange('email_verified', value)} + options={emailVerifiedOptions} + /> + + {clientsModuleActive && ( + + + {imagePreview && ( + + )} +
    +
    +
    +
    + + + + +

    + Informations personnelles +

    +
    + + +
    + {created_at && ( + + )} +
    + + +
    + +
    + + ); +} diff --git a/src/features/auth/components/AuthPages.js b/src/features/auth/components/AuthPages.js new file mode 100644 index 0000000..be0ecaa --- /dev/null +++ b/src/features/auth/components/AuthPages.js @@ -0,0 +1,104 @@ +'use client'; + +/** + * Auth Pages Component - Catch-all route for Next.js App Router + * This component handles all authentication routes: login, register, forgot, reset, confirm + */ + +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import LoginPage from './pages/LoginPage.js'; +import RegisterPage from './pages/RegisterPage.js'; +import ForgotPasswordPage from './pages/ForgotPasswordPage.js'; +import ResetPasswordPage from './pages/ResetPasswordPage.js'; +import ConfirmEmailPage from './pages/ConfirmEmailPage.js'; +import LogoutPage from './pages/LogoutPage.js'; + +export default function AuthPagesClient({ + params, + searchParams, + registerAction, + loginAction, + forgotPasswordAction, + resetPasswordAction, + verifyEmailAction, + logoutAction, + setSessionCookieAction, + redirectAfterLogin = '/', + currentUser = null +}) { + const router = useRouter(); + const [currentPage, setCurrentPage] = useState(null); // null = loading + const [isLoading, setIsLoading] = useState(true); + const [email, setEmail] = useState(''); + const [token, setToken] = useState(''); + + useEffect(() => { + // Get page from params or URL + const getPageFromParams = () => { + if (params?.auth?.[0]) { + return params.auth[0]; + } + + // Fallback: read from URL + if (typeof window !== 'undefined') { + const pathname = window.location.pathname; + const match = pathname.match(/\/auth\/([^\/\?]+)/); + return match ? match[1] : 'login'; + } + + return 'login'; + }; + + const page = getPageFromParams(); + setCurrentPage(page); + setIsLoading(false); + }, [params]); + + // Extract email and token from searchParams (handles both Promise and regular object) + useEffect(() => { + const extractSearchParams = async () => { + let resolvedParams = searchParams; + + // Check if searchParams is a Promise (Next.js 15+) + if (searchParams && typeof searchParams.then === 'function') { + resolvedParams = await searchParams; + } + + // Extract email and token from URL if not in searchParams + if (typeof window !== 'undefined') { + const urlParams = new URLSearchParams(window.location.search); + setEmail(resolvedParams?.email || urlParams.get('email') || ''); + setToken(resolvedParams?.token || urlParams.get('token') || ''); + } else { + setEmail(resolvedParams?.email || ''); + setToken(resolvedParams?.token || ''); + } + }; + + extractSearchParams(); + }, [searchParams]); + + const navigate = (page) => { + router.push(`/auth/${page}`); + }; + + // Don't render anything while determining the correct page + if (isLoading || !currentPage) { + return null; + } + + // Page components mapping + const pageComponents = { + login: () => , + register: () => , + forgot: () => , + reset: () => , + confirm: () => , + logout: () => + }; + + // Render the appropriate page + const PageComponent = pageComponents[currentPage]; + return PageComponent ? : ; +} diff --git a/src/features/auth/components/AuthPagesLayout.js b/src/features/auth/components/AuthPagesLayout.js new file mode 100644 index 0000000..ed8f27f --- /dev/null +++ b/src/features/auth/components/AuthPagesLayout.js @@ -0,0 +1,19 @@ +/** + * Auth Pages Layout - Server Component + * Provides the layout structure for authentication pages + * + * Usage: + * + * + * + */ + +export default function AuthPagesLayout({ children }) { + return ( +
    +
    + {children} +
    +
    + ); +} diff --git a/src/features/auth/components/UserAvatar.js b/src/features/auth/components/UserAvatar.js new file mode 100644 index 0000000..b261743 --- /dev/null +++ b/src/features/auth/components/UserAvatar.js @@ -0,0 +1,55 @@ +'use client'; + +/** + * Displays the current user's avatar (image or initials fallback). + * Image is loaded from /zen/api/storage/{key}; session cookie is sent automatically. + * + * @param {Object} props + * @param {Object} props.user - User object with optional image (storage key) and name + * @param {'sm'|'md'|'lg'} [props.size='md'] - Size of the avatar + * @param {string} [props.className] - Additional CSS classes for the wrapper + */ + +function getImageUrl(imageKey) { + if (!imageKey) return null; + return `/zen/api/storage/${imageKey}`; +} + +function getInitials(name) { + if (!name || !name.trim()) return '?'; + return name + .trim() + .split(/\s+/) + .map((n) => n[0]) + .join('') + .toUpperCase() + .slice(0, 2); +} + +const sizeClasses = { + sm: 'w-8 h-8 text-xs', + md: 'w-10 h-10 text-sm', + lg: 'w-12 h-12 text-base', +}; + +export default function UserAvatar({ user, size = 'md', className = '' }) { + const sizeClass = sizeClasses[size] || sizeClasses.md; + const imageUrl = user?.image ? getImageUrl(user.image) : null; + + return ( +
    + {imageUrl ? ( + {user?.name + ) : ( + {getInitials(user?.name)} + )} +
    + ); +} diff --git a/src/features/auth/components/UserMenu.js b/src/features/auth/components/UserMenu.js new file mode 100644 index 0000000..3174f0d --- /dev/null +++ b/src/features/auth/components/UserMenu.js @@ -0,0 +1,90 @@ +'use client'; + +/** + * User menu: avatar + name with optional dropdown (account link, logout). + * Can receive user from server (e.g. from getSession()) or use useCurrentUser() on client. + * + * @param {Object} props + * @param {Object} [props.user] - User from server (getSession().user). If not provided, useCurrentUser() is used. + * @param {string} [props.accountHref='/dashboard/account'] - Link for "My account" + * @param {string} [props.logoutHref='/auth/logout'] - Link for logout + * @param {string} [props.className] - Extra classes for the menu wrapper + */ + +import { Fragment } from 'react'; +import { Menu, Transition } from '@headlessui/react'; +import UserAvatar from './UserAvatar.js'; +import { useCurrentUser } from './useCurrentUser.js'; + +export default function UserMenu({ + user: userProp, + accountHref = '/dashboard/account', + logoutHref = '/auth/logout', + className = '', +}) { + const { user: userFromHook, loading } = useCurrentUser(); + const user = userProp ?? userFromHook; + + if (loading && !userProp) { + return ( +
    +
    +
    +
    + ); + } + + if (!user) { + return null; + } + + return ( + + + + + {user.name || user.email || 'Account'} + + + + + + + +
    +

    {user.name || 'User'}

    +

    {user.email}

    +
    + + {({ active }) => ( + + My account + + )} + + + {({ active }) => ( + + Log out + + )} + +
    +
    +
    + ); +} diff --git a/src/features/auth/components/index.js b/src/features/auth/components/index.js new file mode 100644 index 0000000..20c2a46 --- /dev/null +++ b/src/features/auth/components/index.js @@ -0,0 +1,43 @@ +/** + * Auth Components Export + * + * Use these components to build custom auth pages for every flow (login, register, forgot, + * reset, confirm, logout) so they match your site's style. + * For a ready-made catch-all auth UI, use AuthPagesClient from '@hykocx/zen/auth/pages'. + * For the default full-page auth (no custom layout), re-export from '@hykocx/zen/auth/page'. + * + * --- Custom auth pages (all types) --- + * + * Pattern: server component loads session/searchParams and passes actions to a client wrapper; + * client wrapper uses useRouter for onNavigate and renders the Zen component. + * + * Component props: + * - LoginPage: onSubmit (loginAction), onSetSessionCookie, onNavigate, redirectAfterLogin, currentUser + * - RegisterPage: onSubmit (registerAction), onNavigate, currentUser + * - ForgotPasswordPage: onSubmit (forgotPasswordAction), onNavigate, currentUser + * - ResetPasswordPage: onSubmit (resetPasswordAction), onNavigate, email, token (from URL) + * - ConfirmEmailPage: onSubmit (verifyEmailAction), onNavigate, email, token (from URL) + * - LogoutPage: onLogout (logoutAction), onSetSessionCookie (optional) + * + * onNavigate receives 'login' | 'register' | 'forgot' | 'reset'. Map to your routes (e.g. /auth/${page}). + * For reset/confirm, pass email and token from searchParams. Full guide: see README-custom-login.md in this package. + * Protect routes with protect() from '@hykocx/zen/auth', redirectTo your login path. + * + * --- Dashboard / user display --- + * + * UserAvatar, UserMenu, AccountSection, useCurrentUser: see README-dashboard.md. + */ + +export { default as AuthPagesLayout } from './AuthPagesLayout.js'; +export { default as AuthPagesClient } from './AuthPages.js'; +export { default as LoginPage } from './pages/LoginPage.js'; +export { default as RegisterPage } from './pages/RegisterPage.js'; +export { default as ForgotPasswordPage } from './pages/ForgotPasswordPage.js'; +export { default as ResetPasswordPage } from './pages/ResetPasswordPage.js'; +export { default as ConfirmEmailPage } from './pages/ConfirmEmailPage.js'; +export { default as LogoutPage } from './pages/LogoutPage.js'; + +export { default as UserAvatar } from './UserAvatar.js'; +export { default as UserMenu } from './UserMenu.js'; +export { default as AccountSection } from './AccountSection.js'; +export { useCurrentUser } from './useCurrentUser.js'; diff --git a/src/features/auth/components/pages/ConfirmEmailPage.js b/src/features/auth/components/pages/ConfirmEmailPage.js new file mode 100644 index 0000000..35b0d96 --- /dev/null +++ b/src/features/auth/components/pages/ConfirmEmailPage.js @@ -0,0 +1,162 @@ +'use client'; + +/** + * Confirm Email Page Component + */ + +import { useState, useEffect, useRef } from 'react'; + +export default function ConfirmEmailPage({ onSubmit, onNavigate, email, token }) { + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [success, setSuccess] = useState(''); + const [hasVerified, setHasVerified] = useState(false); + const isVerifyingRef = useRef(false); + + useEffect(() => { + console.log('ConfirmEmailPage useEffect triggered', { email, token, hasVerified }); + + // Check for persisted success message on mount + const persistedSuccess = sessionStorage.getItem('emailVerificationSuccess'); + console.log('Persisted success message:', persistedSuccess); + + if (persistedSuccess) { + console.log('Restoring persisted success message'); + setSuccess(persistedSuccess); + setIsLoading(false); + setHasVerified(true); // Mark as verified to prevent re-verification + // Clear the persisted message after showing it + sessionStorage.removeItem('emailVerificationSuccess'); + // Redirect after showing the message + setTimeout(() => { + onNavigate('login'); + }, 3000); + return; + } + + // Auto-verify on mount, but only once + if (email && token && !hasVerified && !isVerifyingRef.current) { + console.log('Starting email verification'); + verifyEmail(); + } else if (!email || !token) { + console.log('Invalid email or token'); + setError('Lien de vérification invalide'); + setIsLoading(false); + } + }, [email, token, hasVerified, onNavigate]); + + async function verifyEmail() { + // Prevent multiple calls + if (hasVerified || isVerifyingRef.current) { + console.log('Email verification already attempted or in progress'); + return; + } + + // Set flags IMMEDIATELY to prevent multiple calls + isVerifyingRef.current = true; + setHasVerified(true); + + // Clear any existing states at the start + setError(''); + setSuccess(''); + + console.log('Starting email verification for:', email); + + const formData = new FormData(); + formData.set('email', email); + formData.set('token', token); + + try { + const result = await onSubmit(formData); + console.log('Verification result:', result); + + if (result.success) { + console.log('Verification successful'); + const successMessage = result.message || 'E-mail vérifié avec succès. Vous pouvez maintenant vous connecter.'; + + // Persist success message in sessionStorage + sessionStorage.setItem('emailVerificationSuccess', successMessage); + + setSuccess(successMessage); + setIsLoading(false); + // Redirect to login after 3 seconds + setTimeout(() => { + onNavigate('login'); + }, 3000); + } else { + console.log('Verification failed:', result.error); + setError(result.error || 'Échec de la vérification de l\'e-mail'); + setIsLoading(false); + } + } catch (err) { + console.error('Email verification error:', err); + setError('Une erreur inattendue s\'est produite'); + setIsLoading(false); + } + } + + console.log('ConfirmEmailPage render', { success, error, isLoading, hasVerified }); + + return ( +
    + {/* Header */} +
    +

    + Vérification de l'e-mail +

    +

    + Nous vérifions votre adresse e-mail... +

    +
    + + {isLoading && ( +
    +
    +

    Vérification de votre e-mail en cours...

    +
    + )} + + {/* Success Message - Only show if success and no error */} + {success && !error && ( +
    +
    +
    + {success} +
    +
    + )} + + {/* Error Message - Only show if error and no success */} + {error && !success && ( + + )} + + {/* Redirect message - Only show if success and no error */} + {success && !error && ( +

    Redirection vers la connexion...

    + )} +
    + ); +} + + + diff --git a/src/features/auth/components/pages/ForgotPasswordPage.js b/src/features/auth/components/pages/ForgotPasswordPage.js new file mode 100644 index 0000000..16d3951 --- /dev/null +++ b/src/features/auth/components/pages/ForgotPasswordPage.js @@ -0,0 +1,174 @@ +'use client'; + +/** + * Forgot Password Page Component + */ + +import { useState, useEffect } from 'react'; + +export default function ForgotPasswordPage({ onSubmit, onNavigate, currentUser = null }) { + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [success, setSuccess] = useState(''); + const [formData, setFormData] = useState({ + email: '' + }); + const [honeypot, setHoneypot] = useState(''); + const [formLoadedAt, setFormLoadedAt] = useState(0); + + useEffect(() => { + setFormLoadedAt(Date.now()); + }, []); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + async function handleSubmit(e) { + e.preventDefault(); + setError(''); + setSuccess(''); + setIsLoading(true); + + const submitData = new FormData(); + submitData.append('email', formData.email); + submitData.append('_hp', honeypot); + submitData.append('_t', String(formLoadedAt)); + + try { + const result = await onSubmit(submitData); + + if (result.success) { + setSuccess(result.message); + setIsLoading(false); + } else { + setError(result.error || 'Échec de l\'envoi de l\'e-mail de réinitialisation'); + setIsLoading(false); + } + } catch (err) { + console.error('Forgot password error:', err); + setError('Une erreur inattendue s\'est produite'); + setIsLoading(false); + } + } + + const inputClasses = 'w-full px-3 py-2.5 rounded-lg text-sm focus:outline-none transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20'; + + return ( +
    + {/* Header */} +
    +

    + Mot de passe oublié +

    +

    + Entrez votre adresse e-mail et nous vous enverrons un lien pour réinitialiser votre mot de passe. +

    +
    + + {/* Already Connected Message */} + {currentUser && ( +
    +
    +
    +
    + + Vous êtes connecté en tant que {currentUser.name}.{' '} + + Se déconnecter ? + + +
    +
    +
    + )} + + {/* Error Message */} + {error && ( +
    +
    +
    + {error} +
    +
    + )} + + {/* Success Message */} + {success && ( +
    +
    +
    + {success} +
    +
    + )} + + {/* Forgot Password Form */} +
    + {/* Honeypot — invisible to humans, filled by bots */} + +
    + + +
    + + +
    + + {/* Back to Login Link */} + +
    + ); +} + + + diff --git a/src/features/auth/components/pages/LoginPage.js b/src/features/auth/components/pages/LoginPage.js new file mode 100644 index 0000000..3a46431 --- /dev/null +++ b/src/features/auth/components/pages/LoginPage.js @@ -0,0 +1,228 @@ +'use client'; + +/** + * Login Page Component + */ + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function LoginPage({ onSubmit, onNavigate, onSetSessionCookie, redirectAfterLogin = '/', currentUser = null }) { + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [formData, setFormData] = useState({ + email: '', + password: '' + }); + const [honeypot, setHoneypot] = useState(''); + const [formLoadedAt, setFormLoadedAt] = useState(0); + const router = useRouter(); + + useEffect(() => { + setFormLoadedAt(Date.now()); + }, []); + + // If already logged in, redirect to redirectAfterLogin + useEffect(() => { + if (currentUser) { + router.replace(redirectAfterLogin); + } + }, [currentUser, redirectAfterLogin, router]); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + const handleKeyPress = (e) => { + if (e.key === 'Enter' && !isLoading && !success) { + handleSubmit(); + } + }; + + const handleSubmit = async () => { + setError(''); + setSuccess(''); + setIsLoading(true); + + const submitData = new FormData(); + submitData.append('email', formData.email); + submitData.append('password', formData.password); + submitData.append('_hp', honeypot); + submitData.append('_t', String(formLoadedAt)); + + try { + const result = await onSubmit(submitData); + + if (result.success) { + const successMsg = result.message || 'Connexion réussie ! Redirection...'; + + // Display success message immediately (no page refresh because we didn't set cookie yet) + setSuccess(successMsg); + setIsLoading(false); + + // Wait for user to see the success message + setTimeout(async () => { + // Now set the session cookie (this might cause a refresh, but we're redirecting anyway) + if (result.sessionToken && onSetSessionCookie) { + await onSetSessionCookie(result.sessionToken); + } + // Then navigate + router.push(redirectAfterLogin); + }, 1500); + } else { + setError(result.error || 'Échec de la connexion'); + setIsLoading(false); + } + } catch (err) { + console.error('Login error:', err); + setError('Une erreur inattendue s\'est produite'); + setIsLoading(false); + } + }; + + const inputClasses = 'w-full px-3 py-2.5 rounded-lg text-sm focus:outline-none transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20'; + + return ( +
    + {/* Header */} +
    +

    + Connexion +

    +

    + Veuillez vous connecter pour continuer. +

    +
    + + {/* Already logged in: redirecting (brief message while redirect runs) */} + {currentUser && ( +
    +
    +
    + Redirection... +
    +
    + )} + + {/* Success Message */} + {success && ( +
    +
    +
    + {success} +
    +
    + )} + + {/* Error Message */} + {error && ( +
    +
    +
    + {error} +
    +
    + )} + + {/* Login Form */} +
    + {/* Honeypot — invisible to humans, filled by bots */} + +
    + + +
    + + + + +
    + + {/* Register Link */} + +
    + ); +} + + + diff --git a/src/features/auth/components/pages/LogoutPage.js b/src/features/auth/components/pages/LogoutPage.js new file mode 100644 index 0000000..03f36b3 --- /dev/null +++ b/src/features/auth/components/pages/LogoutPage.js @@ -0,0 +1,117 @@ +'use client'; + +/** + * Logout Page Component + */ + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function LogoutPage({ onLogout, onSetSessionCookie }) { + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + const handleLogout = async () => { + setError(''); + setSuccess(''); + setIsLoading(true); + + try { + // Call the logout action if provided + if (onLogout) { + const result = await onLogout(); + + if (result && !result.success) { + setError(result.error || 'Échec de la déconnexion'); + setIsLoading(false); + return; + } + } + + // Clear session cookie if provided + if (onSetSessionCookie) { + await onSetSessionCookie('', { expires: new Date(0) }); + } + + // Show success message + setSuccess('Vous avez été déconnecté. Redirection...'); + setIsLoading(false); + + // Wait for user to see the success message, then redirect + setTimeout(() => { + router.push('/'); + }, 100); + + } catch (err) { + console.error('Logout error:', err); + setError('Une erreur inattendue s\'est produite lors de la déconnexion'); + setIsLoading(false); + } + }; + + return ( +
    + {/* Header */} +
    +

    + Prêt à vous déconnecter ? +

    +

    + Cela mettra fin à votre session et vous déconnectera de votre compte. +

    +
    + + {/* Success Message */} + {success && ( +
    +
    +
    + {success} +
    +
    + )} + + {/* Error Message */} + {error && ( +
    +
    +
    + {error} +
    +
    + )} + + {/* Logout Button */} +
    + +
    + + {/* Cancel Link */} +
    + Vous avez changé d'avis ? + + Retour + +
    +
    + ); +} diff --git a/src/features/auth/components/pages/RegisterPage.js b/src/features/auth/components/pages/RegisterPage.js new file mode 100644 index 0000000..02df78b --- /dev/null +++ b/src/features/auth/components/pages/RegisterPage.js @@ -0,0 +1,337 @@ +'use client'; + +/** + * Register Page Component + */ + +import { useState, useEffect } from 'react'; +import { PasswordStrengthIndicator } from '../../../../shared/components'; + +export default function RegisterPage({ onSubmit, onNavigate, currentUser = null }) { + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [success, setSuccess] = useState(''); + const [formData, setFormData] = useState({ + name: '', + email: '', + password: '', + confirmPassword: '' + }); + const [honeypot, setHoneypot] = useState(''); + const [formLoadedAt, setFormLoadedAt] = useState(0); + + useEffect(() => { + setFormLoadedAt(Date.now()); + }, []); + + // Validation functions + const validateEmail = (email) => { + const errors = []; + + if (email.length > 254) { + errors.push('L\'e-mail doit contenir 254 caractères ou moins'); + } + + return errors; + }; + + const validatePassword = (password) => { + const errors = []; + + if (password.length < 8) { + errors.push('Le mot de passe doit contenir au moins 8 caractères'); + } + + if (password.length > 128) { + errors.push('Le mot de passe doit contenir 128 caractères ou moins'); + } + + if (!/[A-Z]/.test(password)) { + errors.push('Le mot de passe doit contenir au moins une majuscule'); + } + + if (!/[a-z]/.test(password)) { + errors.push('Le mot de passe doit contenir au moins une minuscule'); + } + + if (!/\d/.test(password)) { + errors.push('Le mot de passe doit contenir au moins un chiffre'); + } + + return errors; + }; + + const validateName = (name) => { + const errors = []; + + if (name.trim().length === 0) { + errors.push('Le nom ne peut pas être vide'); + } + + if (name.length > 100) { + errors.push('Le nom doit contenir 100 caractères ou moins'); + } + + return errors; + }; + + const isFormValid = () => { + const emailErrors = validateEmail(formData.email); + const passwordErrors = validatePassword(formData.password); + const nameErrors = validateName(formData.name); + + return emailErrors.length === 0 && + passwordErrors.length === 0 && + nameErrors.length === 0 && + formData.password === formData.confirmPassword && + formData.email.trim().length > 0; + }; + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + async function handleSubmit(e) { + e.preventDefault(); + setError(''); + setSuccess(''); + setIsLoading(true); + + // Frontend validation + const emailErrors = validateEmail(formData.email); + const passwordErrors = validatePassword(formData.password); + const nameErrors = validateName(formData.name); + + if (emailErrors.length > 0) { + setError(emailErrors[0]); // Show first error + setIsLoading(false); + return; + } + + if (passwordErrors.length > 0) { + setError(passwordErrors[0]); // Show first error + setIsLoading(false); + return; + } + + if (nameErrors.length > 0) { + setError(nameErrors[0]); // Show first error + setIsLoading(false); + return; + } + + // Validate password match + if (formData.password !== formData.confirmPassword) { + setError('Les mots de passe ne correspondent pas'); + setIsLoading(false); + return; + } + + const submitData = new FormData(); + submitData.append('name', formData.name); + submitData.append('email', formData.email); + submitData.append('password', formData.password); + submitData.append('confirmPassword', formData.confirmPassword); + submitData.append('_hp', honeypot); + submitData.append('_t', String(formLoadedAt)); + + try { + const result = await onSubmit(submitData); + + if (result.success) { + setSuccess(result.message); + setIsLoading(false); + } else { + setError(result.error || 'Échec de l\'inscription'); + setIsLoading(false); + } + } catch (err) { + console.error('Registration error:', err); + setError('Une erreur inattendue s\'est produite'); + setIsLoading(false); + } + } + + const inputClasses = 'w-full px-3 py-2.5 rounded-lg text-sm focus:outline-none transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20'; + + return ( +
    + {/* Header */} +
    +

    + Créer un compte +

    +

    + Inscrivez-vous pour commencer. +

    +
    + + {/* Already Connected Message */} + {currentUser && ( +
    +
    +
    +
    + + Vous êtes connecté en tant que {currentUser.name}.{' '} + + Se déconnecter ? + + +
    +
    +
    + )} + + {/* Error Message */} + {error && ( +
    +
    +
    + {error} +
    +
    + )} + + {/* Success Message */} + {success && ( +
    +
    +
    + {success} +
    +
    + )} + + {/* Registration Form */} +
    + {/* Honeypot — invisible to humans, filled by bots */} + +
    + + +
    + +
    + + +
    + +
    + + + +
    + +
    + + +
    + + +
    + + {/* Login Link */} + +
    + ); +} + + + diff --git a/src/features/auth/components/pages/ResetPasswordPage.js b/src/features/auth/components/pages/ResetPasswordPage.js new file mode 100644 index 0000000..f9b4501 --- /dev/null +++ b/src/features/auth/components/pages/ResetPasswordPage.js @@ -0,0 +1,222 @@ +'use client'; + +/** + * Reset Password Page Component + */ + +import { useState } from 'react'; +import { PasswordStrengthIndicator } from '../../../../shared/components'; + +export default function ResetPasswordPage({ onSubmit, onNavigate, email, token }) { + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [success, setSuccess] = useState(''); + const [formData, setFormData] = useState({ + newPassword: '', + confirmPassword: '' + }); + + // Validation functions + const validatePassword = (password) => { + const errors = []; + + if (password.length < 8) { + errors.push('Le mot de passe doit contenir au moins 8 caractères'); + } + + if (password.length > 128) { + errors.push('Le mot de passe doit contenir 128 caractères ou moins'); + } + + if (!/[A-Z]/.test(password)) { + errors.push('Le mot de passe doit contenir au moins une majuscule'); + } + + if (!/[a-z]/.test(password)) { + errors.push('Le mot de passe doit contenir au moins une minuscule'); + } + + if (!/\d/.test(password)) { + errors.push('Le mot de passe doit contenir au moins un chiffre'); + } + + return errors; + }; + + const isFormValid = () => { + const passwordErrors = validatePassword(formData.newPassword); + + return passwordErrors.length === 0 && + formData.newPassword === formData.confirmPassword && + formData.newPassword.length > 0; + }; + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + async function handleSubmit(e) { + e.preventDefault(); + setError(''); + setSuccess(''); + setIsLoading(true); + + // Frontend validation + const passwordErrors = validatePassword(formData.newPassword); + + if (passwordErrors.length > 0) { + setError(passwordErrors[0]); // Show first error + setIsLoading(false); + return; + } + + // Validate password match + if (formData.newPassword !== formData.confirmPassword) { + setError('Les mots de passe ne correspondent pas'); + setIsLoading(false); + return; + } + + const submitData = new FormData(); + submitData.append('newPassword', formData.newPassword); + submitData.append('confirmPassword', formData.confirmPassword); + submitData.append('email', email); + submitData.append('token', token); + + try { + const result = await onSubmit(submitData); + + if (result.success) { + setSuccess(result.message); + setIsLoading(false); + // Redirect to login after 2 seconds + setTimeout(() => { + onNavigate('login'); + }, 2000); + } else { + setError(result.error || 'Échec de la réinitialisation du mot de passe'); + setIsLoading(false); + } + } catch (err) { + console.error('Reset password error:', err); + setError('Une erreur inattendue s\'est produite'); + setIsLoading(false); + } + } + + const inputClasses = 'w-full px-3 py-2.5 rounded-lg text-sm focus:outline-none transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20'; + + return ( +
    + {/* Header */} +
    +

    + Réinitialiser le mot de passe +

    +

    + Saisissez votre nouveau mot de passe ci-dessous. +

    +
    + + {/* Error Message */} + {error && ( +
    +
    +
    + {error} +
    +
    + )} + + {/* Success Message */} + {success && ( +
    +
    +
    + {success} +
    +
    + )} + + {/* Reset Password Form */} +
    +
    + + + +
    + +
    + + +
    + + +
    + + {/* Back to Login Link */} + +
    + ); +} + + + diff --git a/src/features/auth/components/useCurrentUser.js b/src/features/auth/components/useCurrentUser.js new file mode 100644 index 0000000..5d83bf7 --- /dev/null +++ b/src/features/auth/components/useCurrentUser.js @@ -0,0 +1,66 @@ +'use client'; + +/** + * Client hook to fetch the current user from the API. + * Uses session cookie (credentials: 'include'); safe to use in client components. + * + * @returns {{ user: Object|null, loading: boolean, error: string|null, refetch: function }} + * + * @example + * const { user, loading, error, refetch } = useCurrentUser(); + * if (loading) return ; + * if (error) return
    Error: {error}
    ; + * if (!user) return Log in; + * return Hello, {user.name}; + */ + +import { useState, useEffect, useCallback } from 'react'; + +const API_BASE = '/zen/api'; + +export function useCurrentUser() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchUser = useCallback(async () => { + setLoading(true); + setError(null); + try { + const res = await fetch(`${API_BASE}/users/me`, { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json' }, + }); + const data = await res.json(); + + if (!res.ok) { + if (res.status === 401) { + setUser(null); + return; + } + setError(data.message || data.error || 'Failed to load user'); + setUser(null); + return; + } + + if (data.user) { + setUser(data.user); + } else { + setUser(null); + } + } catch (err) { + console.error('[useCurrentUser]', err); + setError(err.message || 'Failed to load user'); + setUser(null); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchUser(); + }, [fetchUser]); + + return { user, loading, error, refetch: fetchUser }; +} diff --git a/src/features/auth/index.js b/src/features/auth/index.js new file mode 100644 index 0000000..967a51a --- /dev/null +++ b/src/features/auth/index.js @@ -0,0 +1,66 @@ +/** + * Zen Authentication Module - Server-side utilities + * + * For client components, use '@hykocx/zen/auth/pages' + * For server actions, use '@hykocx/zen/auth/actions' + */ + +// Authentication library (server-side only) +export { + register, + login, + requestPasswordReset, + resetPassword, + verifyUserEmail, + updateUser +} from './lib/auth.js'; + +// Session management (server-side only) +export { + createSession, + validateSession, + deleteSession, + deleteUserSessions, + refreshSession +} from './lib/session.js'; + +// Email utilities (server-side only) +export { + createEmailVerification, + verifyEmailToken, + createPasswordReset, + verifyResetToken, + deleteResetToken, + sendVerificationEmail, + sendPasswordResetEmail, + sendPasswordChangedEmail +} from './lib/email.js'; + +// Password utilities (server-side only) +export { + hashPassword, + verifyPassword, + generateToken, + generateId +} from './lib/password.js'; + +// Middleware (server-side only) +export { + protect, + checkAuth, + requireRole +} from './middleware/protect.js'; + +// Server Actions (server-side only) +export { + registerAction, + loginAction, + logoutAction, + getSession, + forgotPasswordAction, + resetPasswordAction, + verifyEmailAction, + setSessionCookie, + refreshSessionCookie +} from './actions/authActions.js'; + diff --git a/src/features/auth/lib/auth.js b/src/features/auth/lib/auth.js new file mode 100644 index 0000000..eaeb5bf --- /dev/null +++ b/src/features/auth/lib/auth.js @@ -0,0 +1,295 @@ +/** + * Authentication Logic + * Main authentication functions for user registration, login, and password management + */ + +import { create, findOne, updateById, count } from '../../../core/database/crud.js'; +import { hashPassword, verifyPassword, generateId } from './password.js'; +import { createSession } from './session.js'; +import { createEmailVerification, createPasswordReset, deleteResetToken, sendPasswordChangedEmail } from './email.js'; + +/** + * Register a new user + * @param {Object} userData - User registration data + * @param {string} userData.email - User email + * @param {string} userData.password - User password + * @param {string} userData.name - User name + * @returns {Promise} Created user and session + */ +async function register(userData) { + const { email, password, name } = userData; + + // Validate required fields + if (!email || !password || !name) { + throw new Error('L\'e-mail, le mot de passe et le nom sont requis'); + } + + // Validate email length (maximum 254 characters - RFC standard) + if (email.length > 254) { + throw new Error('L\'e-mail doit contenir 254 caractères ou moins'); + } + + // Validate password length (minimum 8, maximum 128 characters) + if (password.length < 8) { + throw new Error('Le mot de passe doit contenir au moins 8 caractères'); + } + + if (password.length > 128) { + throw new Error('Le mot de passe doit contenir 128 caractères ou moins'); + } + + // Validate password complexity (1 uppercase, 1 lowercase, 1 number) + const hasUppercase = /[A-Z]/.test(password); + const hasLowercase = /[a-z]/.test(password); + const hasNumber = /\d/.test(password); + + if (!hasUppercase || !hasLowercase || !hasNumber) { + throw new Error('Le mot de passe doit contenir au moins une majuscule, une minuscule et un chiffre'); + } + + // Validate name length (maximum 100 characters) + if (name.length > 100) { + throw new Error('Le nom doit contenir 100 caractères ou moins'); + } + + // Validate name is not empty after trimming + if (name.trim().length === 0) { + throw new Error('Le nom ne peut pas être vide'); + } + + // Check if user already exists + const existingUser = await findOne('zen_auth_users', { email }); + if (existingUser) { + throw new Error('Un utilisateur avec cet e-mail existe déjà'); + } + + // Check if this is the first user - if so, make them admin + const userCount = await count('zen_auth_users'); + const role = userCount === 0 ? 'admin' : 'user'; + + // Hash password + const hashedPassword = await hashPassword(password); + + // Create user + const userId = generateId(); + const user = await create('zen_auth_users', { + id: userId, + email, + name, + email_verified: false, + image: null, + role, + updated_at: new Date() + }); + + // Create account with password + const accountId = generateId(); + await create('zen_auth_accounts', { + id: accountId, + account_id: email, + provider_id: 'credential', + user_id: user.id, + password: hashedPassword, + updated_at: new Date() + }); + + // Create email verification token + const verification = await createEmailVerification(email); + + return { + user, + verificationToken: verification.token + }; +} + +/** + * Login a user + * @param {Object} credentials - Login credentials + * @param {string} credentials.email - User email + * @param {string} credentials.password - User password + * @param {Object} sessionOptions - Session options (ipAddress, userAgent) + * @returns {Promise} User and session + */ +async function login(credentials, sessionOptions = {}) { + const { email, password } = credentials; + + // Validate required fields + if (!email || !password) { + throw new Error('L\'e-mail et le mot de passe sont requis'); + } + + // Find user + const user = await findOne('zen_auth_users', { email }); + if (!user) { + throw new Error('E-mail ou mot de passe incorrect'); + } + + // Find account with password + const account = await findOne('zen_auth_accounts', { + user_id: user.id, + provider_id: 'credential' + }); + + if (!account || !account.password) { + throw new Error('E-mail ou mot de passe incorrect'); + } + + // Verify password + const isValid = await verifyPassword(password, account.password); + if (!isValid) { + throw new Error('E-mail ou mot de passe incorrect'); + } + + // Create session + const session = await createSession(user.id, sessionOptions); + + return { + user, + session + }; +} + +/** + * Request a password reset + * @param {string} email - User email + * @returns {Promise} Reset token + */ +async function requestPasswordReset(email) { + // Validate email + if (!email) { + throw new Error('L\'e-mail est requis'); + } + + // Check if user exists + const user = await findOne('zen_auth_users', { email }); + if (!user) { + // Don't reveal if user exists or not + return { success: true }; + } + + // Create password reset token + const reset = await createPasswordReset(email); + + return { + success: true, + token: reset.token + }; +} + +/** + * Reset password with token + * @param {Object} resetData - Reset data + * @param {string} resetData.email - User email + * @param {string} resetData.token - Reset token + * @param {string} resetData.newPassword - New password + * @returns {Promise} Success status + */ +async function resetPassword(resetData) { + const { email, token, newPassword } = resetData; + + // Validate required fields + if (!email || !token || !newPassword) { + throw new Error('L\'e-mail, le jeton et le nouveau mot de passe sont requis'); + } + + // Validate password length (minimum 8, maximum 128 characters) + if (newPassword.length < 8) { + throw new Error('Le mot de passe doit contenir au moins 8 caractères'); + } + + if (newPassword.length > 128) { + throw new Error('Le mot de passe doit contenir 128 caractères ou moins'); + } + + // Validate password complexity (1 uppercase, 1 lowercase, 1 number) + const hasUppercase = /[A-Z]/.test(newPassword); + const hasLowercase = /[a-z]/.test(newPassword); + const hasNumber = /\d/.test(newPassword); + + if (!hasUppercase || !hasLowercase || !hasNumber) { + throw new Error('Le mot de passe doit contenir au moins une majuscule, une minuscule et un chiffre'); + } + + // Verify token is handled in the email module + // For now, we'll assume token is valid if it exists in the database + + // Find user + const user = await findOne('zen_auth_users', { email }); + if (!user) { + throw new Error('Jeton de réinitialisation invalide'); + } + + // Find account + const account = await findOne('zen_auth_accounts', { + user_id: user.id, + provider_id: 'credential' + }); + + if (!account) { + throw new Error('Compte introuvable'); + } + + // Hash new password + const hashedPassword = await hashPassword(newPassword); + + // Update password + await updateById('zen_auth_accounts', account.id, { + password: hashedPassword, + updated_at: new Date() + }); + + // Delete reset token + await deleteResetToken(email); + + // Send password changed confirmation email + try { + await sendPasswordChangedEmail(email); + } catch (error) { + // Log error but don't fail the password reset process + console.error(`[ZEN AUTH] Failed to send password changed email to ${email}:`, error.message); + } + + return { success: true }; +} + +/** + * Verify user email + * @param {string} userId - User ID + * @returns {Promise} Updated user + */ +async function verifyUserEmail(userId) { + return await updateById('zen_auth_users', userId, { + email_verified: true, + updated_at: new Date() + }); +} + +/** + * Update user profile + * @param {string} userId - User ID + * @param {Object} updateData - Data to update + * @returns {Promise} Updated user + */ +async function updateUser(userId, updateData) { + const allowedFields = ['name', 'image', 'language']; + const filteredData = {}; + + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + filteredData[field] = updateData[field]; + } + } + + filteredData.updated_at = new Date(); + + return await updateById('zen_auth_users', userId, filteredData); +} + +export { + register, + login, + requestPasswordReset, + resetPassword, + verifyUserEmail, + updateUser +}; diff --git a/src/features/auth/lib/email.js b/src/features/auth/lib/email.js new file mode 100644 index 0000000..cd5b56b --- /dev/null +++ b/src/features/auth/lib/email.js @@ -0,0 +1,233 @@ +/** + * Email Verification and Password Reset + * Handles email verification tokens and password reset tokens + */ + +import { create, findOne, deleteWhere } from '../../../core/database/crud.js'; +import { generateToken, generateId } from './password.js'; +import { sendAuthEmail } from '../../../core/email/index.js'; +import { renderVerificationEmail, renderPasswordResetEmail, renderPasswordChangedEmail } from '../../../core/email/templates/index.js'; + +/** + * Create an email verification token + * @param {string} email - User email + * @returns {Promise} Verification object with token + */ +async function createEmailVerification(email) { + const token = generateToken(32); + const verificationId = generateId(); + + // Token expires in 24 hours + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 24); + + // Delete any existing verification tokens for this email + await deleteWhere('zen_auth_verifications', { + identifier: 'email_verification', + value: email + }); + + const verification = await create('zen_auth_verifications', { + id: verificationId, + identifier: 'email_verification', + value: email, + token, + expires_at: expiresAt, + updated_at: new Date() + }); + + return { + ...verification, + token + }; +} + +/** + * Verify an email token + * @param {string} email - User email + * @param {string} token - Verification token + * @returns {Promise} True if valid, false otherwise + */ +async function verifyEmailToken(email, token) { + const verification = await findOne('zen_auth_verifications', { + identifier: 'email_verification', + value: email + }); + + if (!verification) return false; + + // Verify token matches + if (verification.token !== token) return false; + + // Check if token is expired + if (new Date(verification.expires_at) < new Date()) { + await deleteWhere('zen_auth_verifications', { id: verification.id }); + return false; + } + + // Delete the verification token after use + await deleteWhere('zen_auth_verifications', { id: verification.id }); + + return true; +} + +/** + * Create a password reset token + * @param {string} email - User email + * @returns {Promise} Reset object with token + */ +async function createPasswordReset(email) { + const token = generateToken(32); + const resetId = generateId(); + + // Token expires in 1 hour + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 1); + + // Delete any existing reset tokens for this email + await deleteWhere('zen_auth_verifications', { + identifier: 'password_reset', + value: email + }); + + const reset = await create('zen_auth_verifications', { + id: resetId, + identifier: 'password_reset', + value: email, + token, + expires_at: expiresAt, + updated_at: new Date() + }); + + return { + ...reset, + token + }; +} + +/** + * Verify a password reset token + * @param {string} email - User email + * @param {string} token - Reset token + * @returns {Promise} True if valid, false otherwise + */ +async function verifyResetToken(email, token) { + const reset = await findOne('zen_auth_verifications', { + identifier: 'password_reset', + value: email + }); + + if (!reset) return false; + + // Verify token matches + if (reset.token !== token) return false; + + // Check if token is expired + if (new Date(reset.expires_at) < new Date()) { + await deleteWhere('zen_auth_verifications', { id: reset.id }); + return false; + } + + return true; +} + +/** + * Delete a password reset token + * @param {string} email - User email + * @returns {Promise} Number of deleted tokens + */ +async function deleteResetToken(email) { + return await deleteWhere('zen_auth_verifications', { + identifier: 'password_reset', + value: email + }); +} + +/** + * Send verification email using Resend + * @param {string} email - User email + * @param {string} token - Verification token + * @param {string} baseUrl - Base URL of the application + */ +async function sendVerificationEmail(email, token, baseUrl) { + const verificationUrl = `${baseUrl}/auth/confirm?email=${encodeURIComponent(email)}&token=${token}`; + const appName = process.env.ZEN_NAME || 'ZEN'; + + const html = await renderVerificationEmail(verificationUrl, email, appName); + + const result = await sendAuthEmail({ + to: email, + subject: `Confirmez votre adresse courriel – ${appName}`, + html + }); + + if (!result.success) { + console.error(`[ZEN AUTH] Failed to send verification email to ${email}:`, result.error); + throw new Error('Failed to send verification email'); + } + + console.log(`[ZEN AUTH] Verification email sent to ${email}`); + return result; +} + +/** + * Send password reset email using Resend + * @param {string} email - User email + * @param {string} token - Reset token + * @param {string} baseUrl - Base URL of the application + */ +async function sendPasswordResetEmail(email, token, baseUrl) { + const resetUrl = `${baseUrl}/auth/reset?email=${encodeURIComponent(email)}&token=${token}`; + const appName = process.env.ZEN_NAME || 'ZEN'; + + const html = await renderPasswordResetEmail(resetUrl, email, appName); + + const result = await sendAuthEmail({ + to: email, + subject: `Réinitialisation du mot de passe – ${appName}`, + html + }); + + if (!result.success) { + console.error(`[ZEN AUTH] Failed to send password reset email to ${email}:`, result.error); + throw new Error('Failed to send password reset email'); + } + + console.log(`[ZEN AUTH] Password reset email sent to ${email}`); + return result; +} + +/** + * Send password changed confirmation email using Resend + * @param {string} email - User email + */ +async function sendPasswordChangedEmail(email) { + const appName = process.env.ZEN_NAME || 'ZEN'; + + const html = await renderPasswordChangedEmail(email, appName); + + const result = await sendAuthEmail({ + to: email, + subject: `Mot de passe modifié – ${appName}`, + html + }); + + if (!result.success) { + console.error(`[ZEN AUTH] Failed to send password changed email to ${email}:`, result.error); + throw new Error('Failed to send password changed email'); + } + + console.log(`[ZEN AUTH] Password changed email sent to ${email}`); + return result; +} + +export { + createEmailVerification, + verifyEmailToken, + createPasswordReset, + verifyResetToken, + deleteResetToken, + sendVerificationEmail, + sendPasswordResetEmail, + sendPasswordChangedEmail +}; diff --git a/src/features/auth/lib/password.js b/src/features/auth/lib/password.js new file mode 100644 index 0000000..0c0a434 --- /dev/null +++ b/src/features/auth/lib/password.js @@ -0,0 +1,65 @@ +/** + * Password Hashing and Verification + * Provides secure password hashing using bcrypt + */ + +import crypto from 'crypto'; + +/** + * Hash a password using scrypt (Node.js native) + * @param {string} password - Plain text password + * @returns {Promise} Hashed password + */ +async function hashPassword(password) { + return new Promise((resolve, reject) => { + // Generate a salt + const salt = crypto.randomBytes(16).toString('hex'); + + // Hash password with salt using scrypt + crypto.scrypt(password, salt, 64, (err, derivedKey) => { + if (err) reject(err); + resolve(salt + ':' + derivedKey.toString('hex')); + }); + }); +} + +/** + * Verify a password against a hash + * @param {string} password - Plain text password + * @param {string} hash - Hashed password + * @returns {Promise} True if password matches, false otherwise + */ +async function verifyPassword(password, hash) { + return new Promise((resolve, reject) => { + const [salt, key] = hash.split(':'); + + crypto.scrypt(password, salt, 64, (err, derivedKey) => { + if (err) reject(err); + resolve(key === derivedKey.toString('hex')); + }); + }); +} + +/** + * Generate a random token + * @param {number} length - Token length in bytes (default: 32) + * @returns {string} Random token + */ +function generateToken(length = 32) { + return crypto.randomBytes(length).toString('hex'); +} + +/** + * Generate a random ID + * @returns {string} Random ID + */ +function generateId() { + return crypto.randomUUID(); +} + +export { + hashPassword, + verifyPassword, + generateToken, + generateId +}; diff --git a/src/features/auth/lib/rateLimit.js b/src/features/auth/lib/rateLimit.js new file mode 100644 index 0000000..18a29c9 --- /dev/null +++ b/src/features/auth/lib/rateLimit.js @@ -0,0 +1,116 @@ +/** + * In-memory rate limiter + * Stores counters in a Map — resets on server restart, no DB required. + */ + +/** @type {Map} */ +const store = new Map(); + +// Purge expired entries every 10 minutes to avoid memory leak +const cleanup = setInterval(() => { + const now = Date.now(); + for (const [key, entry] of store.entries()) { + const windowExpired = now > entry.windowStart + entry.windowMs; + const blockExpired = !entry.blockedUntil || now > entry.blockedUntil; + if (windowExpired && blockExpired) { + store.delete(key); + } + } +}, 10 * 60 * 1000); + +// Allow garbage collection in test/serverless environments +if (cleanup.unref) cleanup.unref(); + +/** + * Rate limit presets per action. + * maxAttempts : number of requests allowed in the window + * windowMs : rolling window duration + * blockMs : how long to block once the limit is exceeded + */ +export const RATE_LIMITS = { + login: { maxAttempts: 5, windowMs: 15 * 60 * 1000, blockMs: 30 * 60 * 1000 }, + register: { maxAttempts: 5, windowMs: 60 * 60 * 1000, blockMs: 60 * 60 * 1000 }, + forgot_password: { maxAttempts: 3, windowMs: 60 * 60 * 1000, blockMs: 60 * 60 * 1000 }, + reset_password: { maxAttempts: 5, windowMs: 15 * 60 * 1000, blockMs: 30 * 60 * 1000 }, + verify_email: { maxAttempts: 10, windowMs: 60 * 60 * 1000, blockMs: 30 * 60 * 1000 }, + api: { maxAttempts: 120, windowMs: 60 * 1000, blockMs: 5 * 60 * 1000 }, +}; + +/** + * Check whether a given identifier is allowed for an action, and record the attempt. + * + * @param {string} identifier - IP address or user ID + * @param {string} action - Key from RATE_LIMITS (e.g. 'login') + * @returns {{ allowed: boolean, retryAfterMs?: number, remaining?: number }} + */ +export function checkRateLimit(identifier, action) { + const config = RATE_LIMITS[action]; + if (!config) return { allowed: true }; + + const key = `${action}:${identifier}`; + const now = Date.now(); + let entry = store.get(key); + + // Still blocked + if (entry?.blockedUntil && now < entry.blockedUntil) { + return { allowed: false, retryAfterMs: entry.blockedUntil - now }; + } + + // Start a fresh window (first request, or previous window has expired) + if (!entry || now > entry.windowStart + entry.windowMs) { + store.set(key, { count: 1, windowStart: now, windowMs: config.windowMs, blockedUntil: null }); + return { allowed: true, remaining: config.maxAttempts - 1 }; + } + + // Increment counter in the current window + entry.count += 1; + + if (entry.count > config.maxAttempts) { + entry.blockedUntil = now + config.blockMs; + store.set(key, entry); + return { allowed: false, retryAfterMs: config.blockMs }; + } + + store.set(key, entry); + return { allowed: true, remaining: config.maxAttempts - entry.count }; +} + +/** + * Extract the best-effort client IP from Next.js headers() (server actions). + * @param {import('next/headers').ReadonlyHeaders} headersList + * @returns {string} + */ +export function getIpFromHeaders(headersList) { + return ( + headersList.get('x-forwarded-for')?.split(',')[0]?.trim() || + headersList.get('x-real-ip') || + 'unknown' + ); +} + +/** + * Extract the best-effort client IP from a Next.js Request object (API routes). + * @param {Request} request + * @returns {string} + */ +export function getIpFromRequest(request) { + return ( + request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || + request.headers.get('x-real-ip') || + 'unknown' + ); +} + +/** + * Format a block duration in human-readable French. + * @param {number} ms + * @returns {string} + */ +export function formatRetryAfter(ms) { + const seconds = Math.ceil(ms / 1000); + if (seconds < 60) return `${seconds} secondes`; + const minutes = Math.ceil(seconds / 60); + if (minutes < 60) return `${minutes} minute${minutes > 1 ? 's' : ''}`; + const hours = Math.ceil(minutes / 60); + return `${hours} heure${hours > 1 ? 's' : ''}`; +} diff --git a/src/features/auth/lib/session.js b/src/features/auth/lib/session.js new file mode 100644 index 0000000..e9197ac --- /dev/null +++ b/src/features/auth/lib/session.js @@ -0,0 +1,138 @@ +/** + * Session Management + * Handles user session creation, validation, and deletion + */ + +import { create, findOne, deleteWhere, updateById } from '../../../core/database/crud.js'; +import { generateToken, generateId } from './password.js'; + +/** + * Create a new session for a user + * @param {string} userId - User ID + * @param {Object} options - Session options (ipAddress, userAgent) + * @returns {Promise} Session object with token + */ +async function createSession(userId, options = {}) { + const { ipAddress, userAgent } = options; + + // Generate session token + const token = generateToken(32); + const sessionId = generateId(); + + // Session expires in 30 days + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + 30); + + const session = await create('zen_auth_sessions', { + id: sessionId, + user_id: userId, + token, + expires_at: expiresAt, + ip_address: ipAddress || null, + user_agent: userAgent || null, + updated_at: new Date() + }); + + return session; +} + +/** + * Validate a session token + * @param {string} token - Session token + * @returns {Promise} Session object with user data or null if invalid + */ +async function validateSession(token) { + if (!token) return null; + + const session = await findOne('zen_auth_sessions', { token }); + + if (!session) return null; + + // Check if session is expired + if (new Date(session.expires_at) < new Date()) { + await deleteSession(token); + return null; + } + + // Get user data + const user = await findOne('zen_auth_users', { id: session.user_id }); + + if (!user) { + await deleteSession(token); + return null; + } + + // Auto-refresh session if it expires in less than 20 days + const now = new Date(); + const expiresAt = new Date(session.expires_at); + const daysUntilExpiry = Math.ceil((expiresAt - now) / (1000 * 60 * 60 * 24)); + + let sessionRefreshed = false; + + if (daysUntilExpiry < 20) { + // Extend session to 30 days from now + const newExpiresAt = new Date(); + newExpiresAt.setDate(newExpiresAt.getDate() + 30); + + await updateById('zen_auth_sessions', session.id, { + expires_at: newExpiresAt, + updated_at: new Date() + }); + + // Update the session object with new expiration + session.expires_at = newExpiresAt; + sessionRefreshed = true; + } + + return { + session, + user, + sessionRefreshed + }; +} + +/** + * Delete a session + * @param {string} token - Session token + * @returns {Promise} Number of deleted sessions + */ +async function deleteSession(token) { + return await deleteWhere('zen_auth_sessions', { token }); +} + +/** + * Delete all sessions for a user + * @param {string} userId - User ID + * @returns {Promise} Number of deleted sessions + */ +async function deleteUserSessions(userId) { + return await deleteWhere('zen_auth_sessions', { user_id: userId }); +} + +/** + * Refresh a session (extend expiration) + * @param {string} token - Session token + * @returns {Promise} Updated session or null + */ +async function refreshSession(token) { + const session = await findOne('zen_auth_sessions', { token }); + + if (!session) return null; + + // Extend session by 30 days + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + 30); + + return await updateById('zen_auth_sessions', session.id, { + expires_at: expiresAt, + updated_at: new Date() + }); +} + +export { + createSession, + validateSession, + deleteSession, + deleteUserSessions, + refreshSession +}; diff --git a/src/features/auth/middleware/protect.js b/src/features/auth/middleware/protect.js new file mode 100644 index 0000000..4bd9cf5 --- /dev/null +++ b/src/features/auth/middleware/protect.js @@ -0,0 +1,83 @@ +/** + * Route Protection Middleware + * Utilities to protect routes and check authentication + */ + +import { getSession } from '../actions/authActions.js'; +import { redirect } from 'next/navigation'; + +/** + * Protect a page - requires authentication + * Use this in server components to require authentication + * + * @param {Object} options - Protection options + * @param {string} options.redirectTo - Where to redirect if not authenticated (default: '/auth/login') + * @returns {Promise} Session object with user data + * + * @example + * // In a server component: + * import { protect } from '@hykocx/zen/auth'; + * + * export default async function ProtectedPage() { + * const session = await protect(); + * return
    Welcome, {session.user.name}!
    ; + * } + */ +async function protect(options = {}) { + const { redirectTo = '/auth/login' } = options; + + const session = await getSession(); + + if (!session) { + redirect(redirectTo); + } + + return session; +} + +/** + * Check if user is authenticated + * Use this when you want to check authentication without forcing a redirect + * + * @returns {Promise} Session object or null if not authenticated + * + * @example + * import { checkAuth } from '@hykocx/zen/auth'; + * + * export default async function Page() { + * const session = await checkAuth(); + * return session ?
    Logged in
    :
    Not logged in
    ; + * } + */ +async function checkAuth() { + return await getSession(); +} + +/** + * Require a specific role + * @param {Array} allowedRoles - Array of allowed roles + * @param {Object} options - Options + * @returns {Promise} Session object + */ +async function requireRole(allowedRoles = [], options = {}) { + const { redirectTo = '/auth/login', forbiddenRedirect = '/' } = options; + + const session = await getSession(); + + if (!session) { + redirect(redirectTo); + } + + if (!allowedRoles.includes(session.user.role)) { + redirect(forbiddenRedirect); + } + + return session; +} + +export { + protect, + checkAuth, + requireRole +}; + diff --git a/src/features/auth/page.js b/src/features/auth/page.js new file mode 100644 index 0000000..90d1c02 --- /dev/null +++ b/src/features/auth/page.js @@ -0,0 +1,46 @@ +/** + * Auth Page - Server Component Wrapper for Next.js App Router + * + * Default auth UI: login, register, forgot, reset, confirm, logout at /auth/[...auth]. + * Re-export in your app: export { default } from '@hykocx/zen/auth/page'; + * + * For custom auth pages (all flows) that match your site style, use components from + * '@hykocx/zen/auth/components' and actions from '@hykocx/zen/auth/actions'. + * See README-custom-login.md in this package. Basic sites can keep using this default page. + */ + +import { AuthPagesClient } from '@hykocx/zen/auth/pages'; +import { + registerAction, + loginAction, + logoutAction, + forgotPasswordAction, + resetPasswordAction, + verifyEmailAction, + setSessionCookie, + getSession +} from '@hykocx/zen/auth/actions'; + +export default async function AuthPage({ params, searchParams }) { + const session = await getSession(); + + return ( +
    +
    + +
    +
    + ); +} diff --git a/src/features/auth/pages.js b/src/features/auth/pages.js new file mode 100644 index 0000000..2b2d80f --- /dev/null +++ b/src/features/auth/pages.js @@ -0,0 +1,12 @@ +'use client'; + +/** + * Auth Pages Export for Next.js App Router + * + * This exports the auth client components. + * Users must create their own server component wrapper that imports the actions. + */ + +export { default as AuthPagesClient } from './components/AuthPages.js'; +export { default as AuthPagesLayout } from './components/AuthPagesLayout.js'; + diff --git a/src/features/provider/ZenProvider.js b/src/features/provider/ZenProvider.js new file mode 100644 index 0000000..f2a8481 --- /dev/null +++ b/src/features/provider/ZenProvider.js @@ -0,0 +1,12 @@ +'use client'; + +import { ToastProvider, ToastContainer } from '@hykocx/zen/toast'; + +export function ZenProvider({ children }) { + return ( + + {children} + + + ); +} diff --git a/src/features/provider/index.js b/src/features/provider/index.js new file mode 100644 index 0000000..ff80398 --- /dev/null +++ b/src/features/provider/index.js @@ -0,0 +1,3 @@ +'use client'; + +export { ZenProvider } from './ZenProvider.js'; diff --git a/src/features/setup/cli.js b/src/features/setup/cli.js new file mode 100644 index 0000000..f9352d4 --- /dev/null +++ b/src/features/setup/cli.js @@ -0,0 +1,251 @@ +#!/usr/bin/env node + +/** + * Zen Setup CLI + * Command-line tool for setting up Zen in a Next.js project + */ + +import { mkdir, writeFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import readline from 'readline'; + +// File templates +const templates = { + instrumentation: `// instrumentation.js +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + const { initializeZen } = await import('@hykocx/zen'); + await initializeZen(); + } +} +`, + authRedirect: `import { redirect } from 'next/navigation'; + +export default function Redirect() { + redirect('/auth/login/'); +} +`, + authCatchAll: `export { default } from '@hykocx/zen/auth/page'; +`, + adminRedirect: `import { redirect } from 'next/navigation'; + +export default function Redirect() { + redirect('/admin/dashboard'); +} +`, + adminCatchAll: `export { default } from '@hykocx/zen/admin/page'; +`, + zenApiRoute: `export { GET, POST, PUT, DELETE, PATCH } from '@hykocx/zen/zen/api'; +`, + zenPageRoute: `export { default, generateMetadata } from '@hykocx/zen/modules/page'; +`, + nextConfig: `// next.config.js +module.exports = { + experimental: { + instrumentationHook: true, + }, +}; +`, +}; + +// File definitions +const files = [ + { + path: 'instrumentation.js', + template: 'instrumentation', + description: 'Instrumentation file (initialize Zen)', + }, + { + path: 'app/(auth)/auth/page.js', + template: 'authRedirect', + description: 'Auth redirect page', + }, + { + path: 'app/(auth)/auth/[...auth]/page.js', + template: 'authCatchAll', + description: 'Auth catch-all route', + }, + { + path: 'app/(admin)/admin/page.js', + template: 'adminRedirect', + description: 'Admin redirect page', + }, + { + path: 'app/(admin)/admin/[...admin]/page.js', + template: 'adminCatchAll', + description: 'Admin catch-all route', + }, + { + path: 'app/zen/api/[...path]/route.js', + template: 'zenApiRoute', + description: 'Zen API catch-all route', + }, + { + path: 'app/zen/[...zen]/page.js', + template: 'zenPageRoute', + description: 'Zen public pages catch-all route', + }, +]; + +async function createFile(filePath, content, force = false) { + const fullPath = resolve(process.cwd(), filePath); + + // Check if file already exists + if (existsSync(fullPath) && !force) { + console.log(`⏭️ Skipped (already exists): ${filePath}`); + return { created: false, skipped: true }; + } + + // Create directory if it doesn't exist + const dir = dirname(fullPath); + await mkdir(dir, { recursive: true }); + + // Write the file + await writeFile(fullPath, content, 'utf-8'); + console.log(`✅ Created: ${filePath}`); + + return { created: true, skipped: false }; +} + +async function setupZen(options = {}) { + const { force = false } = options; + + console.log('🚀 Setting up Zen for your Next.js project...\n'); + + let created = 0; + let skipped = 0; + + for (const file of files) { + const result = await createFile( + file.path, + templates[file.template], + force + ); + + if (result.created) created++; + if (result.skipped) skipped++; + } + + console.log('\n📝 Summary:'); + console.log(` ✅ Created: ${created} file${created !== 1 ? 's' : ''}`); + console.log(` ⏭️ Skipped: ${skipped} file${skipped !== 1 ? 's' : ''}`); + + // Check if next.config.js needs updating + const nextConfigPath = resolve(process.cwd(), 'next.config.js'); + const nextConfigExists = existsSync(nextConfigPath); + + if (!nextConfigExists) { + console.log('\n⚠️ Note: next.config.js not found.'); + console.log(' Make sure to enable instrumentation in your Next.js config:'); + console.log(' experimental: { instrumentationHook: true }'); + } + + console.log('\n🎉 Setup complete!'); + console.log('\nNext steps:'); + console.log(' 1. Add Zen styles to your globals.css:'); + console.log(' @import \'@hykocx/zen/styles/zen.css\';'); + console.log(' 2. Configure environment variables (see .env.example)'); + console.log(' 3. Initialize the database:'); + console.log(' npx zen-db init'); + console.log('\nFor more information, check the INSTALL.md file.'); +} + +async function listFiles() { + console.log('📋 Files that will be created:\n'); + + for (const file of files) { + const exists = existsSync(resolve(process.cwd(), file.path)); + const status = exists ? '✓ exists' : '✗ missing'; + console.log(` ${status} ${file.path}`); + console.log(` ${file.description}`); + } + + console.log('\nRun "npx zen-setup init" to create missing files.'); +} + +async function runCLI() { + const command = process.argv[2]; + const flags = process.argv.slice(3); + const force = flags.includes('--force') || flags.includes('-f'); + + if (!command || command === 'help') { + console.log(` +Zen Setup CLI + +Usage: + npx zen-setup [options] + +Commands: + init Create all required files for Zen setup + list List all files that will be created + help Show this help message + +Options: + --force, -f Force overwrite existing files + +Examples: + npx zen-setup init # Create missing files + npx zen-setup init --force # Overwrite all files + npx zen-setup list # List all files + `); + process.exit(0); + } + + try { + switch (command) { + case 'init': + if (force) { + console.log('⚠️ WARNING: --force flag will overwrite existing files!\n'); + console.log('Type "yes" to confirm or Ctrl+C to cancel...'); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + rl.question('Confirm (yes/no): ', async (answer) => { + if (answer.toLowerCase() === 'yes') { + await setupZen({ force: true }); + } else { + console.log('❌ Operation cancelled.'); + } + rl.close(); + process.exit(0); + }); + return; // Don't exit yet + } else { + await setupZen({ force: false }); + } + break; + + case 'list': + await listFiles(); + break; + + default: + console.log(`❌ Unknown command: ${command}`); + console.log('Run "npx zen-setup help" for usage information.'); + process.exit(1); + } + + process.exit(0); + + } catch (error) { + console.error('❌ Error:', error.message); + console.error(error.stack); + process.exit(1); + } +} + +// Run CLI if called directly +import { fileURLToPath } from 'url'; +import { realpathSync } from 'node:fs'; +const __filename = realpathSync(fileURLToPath(import.meta.url)); +const isMainModule = process.argv[1] && realpathSync(process.argv[1]) === __filename; + +if (isMainModule) { + runCLI(); +} + +export { runCLI, setupZen }; diff --git a/src/features/setup/index.js b/src/features/setup/index.js new file mode 100644 index 0000000..d2d5c2f --- /dev/null +++ b/src/features/setup/index.js @@ -0,0 +1,6 @@ +/** + * Zen Setup Module + * Utilities for setting up Zen in a Next.js project + */ + +export { setupZen } from './cli.js'; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..6fe317e --- /dev/null +++ b/src/index.js @@ -0,0 +1,48 @@ +// Export database utilities as namespace +export * as db from "./core/database/index.js"; + +// Export authentication module as namespace +export * as auth from "./features/auth/index.js"; + +// Export admin module as namespace +export * as admin from "./features/admin/index.js"; + +// Export API module as namespace +export * as api from "./core/api/index.js"; + +// Export email utilities as namespace +export * as email from "./core/email/index.js"; + +// Export cron utilities as namespace +export * as cron from "./core/cron/index.js"; + +// Export payment utilities as namespace +export * as payments from "./core/payments/index.js"; +export * as stripe from "./core/payments/stripe.js"; + +// Export PDF utilities as namespace +export * as pdf from "./core/pdf/index.js"; + +// Export module system as namespace +export * as moduleSystem from "./core/modules/index.js"; + +// NOTE: Toast components are CLIENT ONLY - import from '@hykocx/zen/toast' +// Do not export here to avoid mixing client/server boundaries + +// Export modules system as namespace (legacy, includes invoice module) +export * as modules from "./modules/index.js"; + +// Export public pages (Zen routes) +export { PublicPagesLayout, PublicPagesClient } from "./modules/pages.js"; + +// Export app configuration utilities +export { getAppName, getAppConfig, getSessionCookieName, getModulesConfig, getPublicBaseUrl } from "./shared/lib/appConfig.js"; + +// Export initialization utilities +export { initializeZen, resetZenInitialization } from "./shared/lib/init.js"; + +// Export date utilities +export * as dates from "./shared/lib/dates.js"; + +// Export currency utilities +export * as currency from "./shared/utils/currency.js"; \ No newline at end of file diff --git a/src/modules/PublicPagesClient.js b/src/modules/PublicPagesClient.js new file mode 100644 index 0000000..f43b513 --- /dev/null +++ b/src/modules/PublicPagesClient.js @@ -0,0 +1,54 @@ +'use client'; + +import React, { Suspense } from 'react'; +import { getModulePublicPageLoader } from './modules.pages.js'; +import { Loading } from '../shared/components'; + +/** + * Not Found Message Component + */ +function NotFoundMessage() { + return ( +
    +
    +

    Page non trouvée

    +

    + La page que vous recherchez n'existe pas. +

    +
    +
    + ); +} + +/** + * Public Module Pages Router + * Handles routing for all public module pages dynamically + * + * Uses the client-side page loader from modules.pages.js instead of + * the server-side registry (which is empty on the client). + */ +const PublicPagesClient = ({ + path = [], + moduleActions = {}, + ...additionalProps +}) => { + const moduleName = path[0]; + const PublicPage = getModulePublicPageLoader(moduleName); + + if (PublicPage) { + return ( + }> + + + ); + } + + // Module not found or no public pages + return ; +}; + +export default PublicPagesClient; diff --git a/src/modules/PublicPagesLayout.js b/src/modules/PublicPagesLayout.js new file mode 100644 index 0000000..b88c4b9 --- /dev/null +++ b/src/modules/PublicPagesLayout.js @@ -0,0 +1,17 @@ +'use client'; + +import React from 'react'; + +/** + * Public Module Pages Layout + * Simple layout for public module pages like invoice payment + */ +const PublicPagesLayout = ({ children }) => { + return ( +
    + {children} +
    + ); +}; + +export default PublicPagesLayout; diff --git a/src/modules/README.md b/src/modules/README.md new file mode 100644 index 0000000..81f70c1 --- /dev/null +++ b/src/modules/README.md @@ -0,0 +1,284 @@ +# Module System + +Modules are self-contained features that can be enabled/disabled via environment variables. + +## File Structure + +``` +src/modules/your-module/ +├── module.config.js # Required — navigation, pages, widgets +├── db.js # Database schema (createTables / dropTables) +├── crud.js # CRUD operations +├── actions.js # Server actions (for public pages) +├── metadata.js # SEO metadata generators +├── api.js # API route handlers +├── cron.config.js # Scheduled tasks +├── index.js # Public API re-exports +├── .env.example # Environment variable documentation +├── admin/ # Admin pages (lazy-loaded) +│ └── index.js # Re-exports admin components +├── pages/ # Public pages (lazy-loaded) +│ └── index.js +├── dashboard/ # Dashboard widgets +│ ├── statsActions.js +│ └── Widget.js +└── sub-feature/ # Optional sub-modules (e.g. items/, categories/) + ├── db.js + ├── crud.js + └── admin/ +``` + +> Not all files are required. Only create what the module actually needs. + +--- + +## Step 1 — Create `module.config.js` + +```javascript +import { lazy } from 'react'; + +export default { + // Module identity + name: 'your-module', + displayName: 'Your Module', + version: '1.0.0', + description: 'Description of your module', + + // Other modules this one depends on (must be enabled too) + dependencies: ['clients'], + + // Environment variables this module uses (documentation only) + envVars: [ + 'YOUR_MODULE_API_KEY', + ], + + // Admin navigation — single section object or array of section objects + navigation: { + id: 'your-module', + title: 'Your Module', + icon: 'SomeIcon', // String icon name from shared/Icons.js + items: [ + { name: 'Items', href: '/admin/your-module/items', icon: 'PackageIcon' }, + { name: 'New', href: '/admin/your-module/items/new', icon: 'PlusSignIcon' }, + ], + }, + + // Admin pages — path → lazy component + adminPages: { + '/admin/your-module/items': lazy(() => import('./admin/ItemsListPage.js')), + '/admin/your-module/items/new': lazy(() => import('./admin/ItemCreatePage.js')), + '/admin/your-module/items/edit': lazy(() => import('./admin/ItemEditPage.js')), + }, + + // (Optional) Custom resolver for dynamic paths not known at build time. + // Called before the adminPages map. Return the lazy component or null. + pageResolver(path) { + const parts = path.split('/').filter(Boolean); + // example: /admin/your-module/{type}/list + if (parts[2] === 'list') return lazy(() => import('./admin/ItemsListPage.js')); + return null; + }, + + // Public pages — keyed by 'default' (one component handles all public routes) + publicPages: { + default: lazy(() => import('./pages/YourModulePublicPages.js')), + }, + + // Public route patterns for SEO/route matching (relative to /zen/your-module/) + publicRoutes: [ + { pattern: ':id', description: 'View item' }, + { pattern: ':id/pdf', description: 'PDF viewer' }, + ], + + // Dashboard widgets (lazy-loaded, rendered on the admin dashboard) + dashboardWidgets: [ + lazy(() => import('./dashboard/Widget.js')), + ], +}; +``` + +### Navigation as multiple sections + +When a module provides several distinct sections (like the `posts` module with one section per post type), set `navigation` to an array: + +```javascript +navigation: [ + { id: 'your-module-foo', title: 'Foo', icon: 'Book02Icon', items: [...] }, + { id: 'your-module-bar', title: 'Bar', icon: 'Layers01Icon', items: [...] }, +], +``` + +--- + +## Step 2 — Create `db.js` + +Every module that uses a database must expose a `createTables` function: + +```javascript +import { query } from '@hykocx/zen/database'; + +export async function createTables() { + const created = []; + const skipped = []; + + const exists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = $1 + )`, ['zen_your_module']); + + if (!exists.rows[0].exists) { + await query(` + CREATE TABLE zen_your_module ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP + ) + `); + created.push('zen_your_module'); + } else { + skipped.push('zen_your_module'); + } + + return { created, skipped }; +} + +export async function dropTables() { + await query(`DROP TABLE IF EXISTS zen_your_module CASCADE`); +} +``` + +> **Never create migrations.** Instead, provide the SQL to the user so they can run it manually. + +--- + +## Step 3 — Create `.env.example` + +Document every environment variable the module reads: + +```bash +################################# +# MODULE YOUR-MODULE +ZEN_MODULE_YOUR_MODULE=false + +ZEN_MODULE_YOUR_MODULE_API_KEY= +ZEN_MODULE_YOUR_MODULE_SOME_OPTION=default_value +################################# +``` + +--- + +## Step 4 — Create `cron.config.js` (optional) + +Only needed if the module requires scheduled tasks: + +```javascript +import { doSomething } from './reminders.js'; + +export default { + jobs: [ + { + name: 'your-module-task', + description: 'Description of what this job does', + schedule: '*/5 * * * *', // cron expression + handler: doSomething, + timezone: process.env.ZEN_TIMEZONE || 'America/Toronto', + }, + ], +}; +``` + +--- + +## Step 5 — Register the module in 5 files + +### `modules/modules.registry.js` — add the module name + +```javascript +export const AVAILABLE_MODULES = [ + 'clients', + 'invoice', + 'your-module', +]; +``` + +### `modules/modules.pages.js` — import the config + +```javascript +'use client'; + +import yourModuleConfig from './your-module/module.config.js'; + +const MODULE_CONFIGS = { + // ...existing modules... + 'your-module': yourModuleConfig, +}; +``` + +### `modules/modules.actions.js` — import server actions (if public pages or dashboard widgets) + +```javascript +import { yourPublicAction } from './your-module/actions.js'; +import { getYourModuleDashboardStats } from './your-module/dashboard/statsActions.js'; + +export const MODULE_ACTIONS = { + // ...existing modules... + 'your-module': { yourPublicAction }, +}; + +export const MODULE_DASHBOARD_ACTIONS = { + // ...existing modules... + 'your-module': getYourModuleDashboardStats, +}; +``` + +### `modules/modules.metadata.js` — import metadata generators (if SEO needed) + +```javascript +import * as yourModuleMetadata from './your-module/metadata.js'; + +export const MODULE_METADATA = { + // ...existing modules... + 'your-module': yourModuleMetadata, +}; +``` + +### `modules/init.js` — register the database initializer + +```javascript +import { createTables as createYourModuleTables } from './your-module/db.js'; + +const MODULE_DB_INITIALIZERS = { + // ...existing modules... + 'your-module': createYourModuleTables, +}; +``` + +--- + +## Step 6 — Enable the module + +```bash +ZEN_MODULE_YOUR_MODULE=true +``` + +The environment variable is derived from the module name: `ZEN_MODULE_` + module name uppercased (hyphens become underscores). + +--- + +## Sub-modules + +For complex modules, group related features into sub-directories. Each sub-module follows the same pattern (`db.js`, `crud.js`, `admin/`). The parent `module.config.js` registers all sub-module admin pages, and the parent `index.js` re-exports everything publicly. + +See `src/modules/invoice/` for a complete example with `items/`, `categories/`, `transactions/`, and `recurrences/` sub-modules. + +--- + +## Reference implementations + +| Module | Features demonstrated | +|--------|-----------------------| +| `src/modules/invoice/` | Sub-modules, public pages, cron jobs, Stripe, email, PDF, dashboard widgets, metadata | +| `src/modules/posts/` | Dynamic config from env vars, `pageResolver`, multiple navigation sections | +| `src/modules/clients/` | Simple module, dependencies, no public pages | diff --git a/src/modules/clients/.env.example b/src/modules/clients/.env.example new file mode 100644 index 0000000..a42bafa --- /dev/null +++ b/src/modules/clients/.env.example @@ -0,0 +1,4 @@ +################################# +# MODULE CLIENTS +ZEN_MODULE_CLIENTS=false +################################# \ No newline at end of file diff --git a/src/modules/clients/INSTALL.md b/src/modules/clients/INSTALL.md new file mode 100644 index 0000000..d73b609 --- /dev/null +++ b/src/modules/clients/INSTALL.md @@ -0,0 +1,37 @@ +# Clients Module Installation + +## 1. Configure Environment Variables + +Copy all variables from [`.env.example`](.env.example) and add them to your `.env` file. + +## 2. Activate the Module + +In your `.env` file, set: + +```env +ZEN_MODULE_CLIENTS=true +``` + +## 3. Database Setup + +Run the database initialization to create the required tables: + +```bash +npx zen-db init +``` + +This will create the following table: +- `zen_clients` - Stores client information + +## 4. Features + +### Client Management +- Create, edit, and delete clients +- Store contact information (name, email, phone, address) +- Link clients to user accounts (optional) +- Unique client numbers (auto-generated) + +### Used By Other Modules +The clients module is a dependency for: +- **Quote Module**: Assign quotes to clients +- **Invoice Module**: Assign invoices to clients diff --git a/src/modules/clients/admin/ClientCreatePage.js b/src/modules/clients/admin/ClientCreatePage.js new file mode 100644 index 0000000..92b97c1 --- /dev/null +++ b/src/modules/clients/admin/ClientCreatePage.js @@ -0,0 +1,76 @@ +'use client'; + +import React, { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Button } from '../../../shared/components'; +import ClientForm from './ClientForm.js'; +import { useToast } from '@hykocx/zen/toast'; + +/** + * Client Create Page Component + * Page for creating a new client + */ +const ClientCreatePage = ({ user }) => { + const router = useRouter(); + const toast = useToast(); + const [saving, setSaving] = useState(false); + + const handleSubmit = async (formData) => { + try { + setSaving(true); + + const response = await fetch('/zen/api/admin/clients', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ client: formData }) + }); + + const data = await response.json(); + + if (data.success) { + toast.success('Client créé avec succès'); + router.push('/admin/clients/list'); + } else { + toast.error(data.message || 'Échec de la création du client'); + } + } catch (error) { + console.error('Error creating client:', error); + toast.error('Échec de la création du client'); + } finally { + setSaving(false); + } + }; + + return ( +
    + {/* Header */} +
    +
    +

    Créer un client

    +

    Remplissez les détails pour créer un nouveau client

    +
    + +
    + + {/* Form */} + router.push('/admin/clients/list')} + isEdit={false} + saving={saving} + users={[]} + /> +
    + ); +}; + +export default ClientCreatePage; diff --git a/src/modules/clients/admin/ClientEditPage.js b/src/modules/clients/admin/ClientEditPage.js new file mode 100644 index 0000000..694b948 --- /dev/null +++ b/src/modules/clients/admin/ClientEditPage.js @@ -0,0 +1,139 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { Button, Card, Loading } from '../../../shared/components'; +import ClientForm from './ClientForm.js'; +import { useToast } from '@hykocx/zen/toast'; + +/** + * Client Edit Page Component + * Page for editing an existing client + */ +const ClientEditPage = ({ clientId, user }) => { + const router = useRouter(); + const toast = useToast(); + const [client, setClient] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + loadClient(); + }, [clientId]); + + const loadClient = async () => { + try { + setLoading(true); + + const response = await fetch(`/zen/api/admin/clients?id=${clientId}`, { + credentials: 'include' + }); + const data = await response.json(); + + if (data.success) { + setClient(data.client); + } else { + toast.error(data.error || 'Échec du chargement du client'); + } + } catch (error) { + console.error('Error loading client:', error); + toast.error('Échec du chargement du client'); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async (formData) => { + try { + setSaving(true); + + const response = await fetch(`/zen/api/admin/clients`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ id: clientId, client: formData }) + }); + + const data = await response.json(); + + if (data.success) { + toast.success('Client mis à jour avec succès'); + router.push('/admin/clients/list'); + } else { + toast.error(data.message || 'Échec de la mise à jour du client'); + } + } catch (error) { + console.error('Error updating client:', error); + toast.error('Échec de la mise à jour du client'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
    + +
    + ); + } + + if (!client) { + return ( +
    +
    +
    +

    Modifier le client

    +

    Client non trouvé

    +
    + +
    + +
    +

    Client non trouvé

    +

    Le client que vous recherchez n'existe pas ou a été supprimé.

    +
    +
    +
    + ); + } + + return ( +
    + {/* Header */} +
    +
    +

    Modifier le client

    +

    Client : {client.first_name} {client.last_name}

    +
    + +
    + + {/* Form */} + router.push('/admin/clients/list')} + isEdit={true} + saving={saving} + users={[]} + /> +
    + ); +}; + +export default ClientEditPage; diff --git a/src/modules/clients/admin/ClientForm.js b/src/modules/clients/admin/ClientForm.js new file mode 100644 index 0000000..1871f7e --- /dev/null +++ b/src/modules/clients/admin/ClientForm.js @@ -0,0 +1,245 @@ +'use client'; + +import React, { useState } from 'react'; +import { Button, Card, Input, Select, Textarea } from '../../../shared/components'; + +/** + * Client Form Component + * Form for creating and editing clients + */ +const ClientForm = ({ + initialData = null, + users = [], + onSubmit, + onCancel, + isEdit = false, + saving = false +}) => { + const [formData, setFormData] = useState({ + user_id: initialData?.user_id || '', + company_name: initialData?.company_name || '', + first_name: initialData?.first_name || '', + last_name: initialData?.last_name || '', + email: initialData?.email || '', + phone: initialData?.phone || '', + address: initialData?.address || '', + city: initialData?.city || '', + province: initialData?.province || '', + postal_code: initialData?.postal_code || '', + country: initialData?.country || 'Canada', + notes: initialData?.notes || '', + }); + + const [errors, setErrors] = useState({}); + + const handleInputChange = (field, value) => { + setFormData(prev => ({ + ...prev, + [field]: value + })); + + if (errors[field]) { + setErrors(prev => ({ + ...prev, + [field]: null + })); + } + }; + + const validateForm = () => { + const newErrors = {}; + + if (!formData.first_name.trim()) { + newErrors.first_name = "Le prénom est requis"; + } + + if (!formData.last_name.trim()) { + newErrors.last_name = "Le nom de famille est requis"; + } + + if (!formData.email.trim()) { + newErrors.email = "L'email est requis"; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = "Format d'email invalide"; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = (e) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + if (onSubmit) { + onSubmit(formData); + } + }; + + return ( +
    + {/* Basic Information */} + +
    +

    Informations de base

    + +
    +
    + handleInputChange('company_name', value)} + placeholder="Entrez le nom de la société..." + /> +
    + + handleInputChange('first_name', value)} + placeholder="Entrez le prénom..." + error={errors.first_name} + /> + + handleInputChange('last_name', value)} + placeholder="Entrez le nom de famille..." + error={errors.last_name} + /> + + handleInputChange('email', value)} + placeholder="Entrez le courriel..." + error={errors.email} + /> + + handleInputChange('phone', value)} + placeholder="Entrez le numéro de téléphone..." + /> +
    +
    +
    + + {/* Address */} + +
    +

    Adresse

    + +
    +
    + handleInputChange('address', value)} + placeholder="Entrez l'adresse..." + /> +
    + + handleInputChange('city', value)} + placeholder="Entrez la ville..." + /> + + handleInputChange('province', value)} + placeholder="Entrez la province ou l'état..." + /> + + handleInputChange('postal_code', value)} + placeholder="Entrez le code postal..." + /> + + handleInputChange('country', value)} + placeholder="Entrez le pays..." + /> +
    +
    +
    + + {/* User Link */} + {users.length > 0 && ( + +
    +

    Lien compte utilisateur

    +

    + Vous pouvez associer ce client à un compte utilisateur sur la plateforme +

    + +