From 65ae3c67883d31974758c9c2b19f62336b1d8e47 Mon Sep 17 00:00:00 2001 From: Hyko Date: Sun, 12 Apr 2026 12:50:14 -0400 Subject: [PATCH] chore: import codes --- .env.example | 47 + .gitignore | 22 + .npmrc | 1 + LICENSE | 232 + README copy.md | 21 + 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 + 241 files changed, 48834 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 LICENSE create mode 100644 README copy.md 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..f3a9cab --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@hykocx:registry=https://git.hyko.cx/api/packages/hykocx/npm/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b9d5e92 --- /dev/null +++ b/LICENSE @@ -0,0 +1,232 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. + + zen + Copyright (C) 2026 hykocx + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + zen Copyright (C) 2026 hykocx + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/README copy.md b/README copy.md new file mode 100644 index 0000000..6360b62 --- /dev/null +++ b/README copy.md @@ -0,0 +1,21 @@ +# zen + +Un CMS construit sur l'essentiel, rien de plus, rien de moins. + +> [!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. + +## 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/README.md b/README.md index f6256da..6360b62 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 +Un CMS construit sur l'essentiel, rien de plus, rien de moins. + +> [!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. + +## 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..791884b --- /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/api/packages/hykocx/npm/" + }, + "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 +

    + +