create-snapify-app 0.0.0-stage → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +24 -0
- package/README.md +55 -2
- package/bin/create-snapify-app.mjs +174 -0
- package/lib/args.mjs +59 -0
- package/lib/copy.mjs +44 -0
- package/lib/env.mjs +84 -0
- package/lib/prompt.mjs +63 -0
- package/package.json +35 -4
- package/template/.env.example +79 -0
- package/template/.github/workflows/ci.yml +23 -0
- package/template/.github/workflows/deploy.yml +26 -0
- package/template/.github/workflows/ping.yml +8 -0
- package/template/.github/workflows/release.yml +67 -0
- package/template/.snapify-install.json +5 -0
- package/template/CHANGELOG.md +28 -0
- package/template/LICENSE +14 -0
- package/template/README.md +287 -0
- package/template/administrator.html +23 -0
- package/template/apps/client/src/App.tsx +765 -0
- package/template/apps/client/src/auth.css +400 -0
- package/template/apps/client/src/lib/api.ts +22 -0
- package/template/apps/client/src/main.tsx +20 -0
- package/template/apps/community/src/App.tsx +592 -0
- package/template/apps/community/src/main.tsx +20 -0
- package/template/apps/community/src/styles.css +24 -0
- package/template/apps/developers/src/App.tsx +643 -0
- package/template/apps/developers/src/main.tsx +17 -0
- package/template/apps/developers/src/pages.tsx +1587 -0
- package/template/apps/developers/src/styles.css +98 -0
- package/template/apps/developers/src/ui.tsx +179 -0
- package/template/apps/landing/src/App.tsx +598 -0
- package/template/apps/landing/src/LegalPages.tsx +158 -0
- package/template/apps/landing/src/OrderPage.tsx +381 -0
- package/template/apps/landing/src/SeoPages.tsx +923 -0
- package/template/apps/landing/src/copy.ts +195 -0
- package/template/apps/landing/src/main.tsx +20 -0
- package/template/apps/landing/src/styles.css +803 -0
- package/template/apps/landing/src/types.ts +19 -0
- package/template/apps/marketplace/src/App.tsx +1229 -0
- package/template/apps/marketplace/src/developer/DeveloperApp.tsx +1266 -0
- package/template/apps/marketplace/src/main.tsx +20 -0
- package/template/apps/marketplace/src/styles.css +1001 -0
- package/template/apps/shared/AuthContext.tsx +65 -0
- package/template/apps/shared/Logo.tsx +33 -0
- package/template/apps/shared/api.ts +47 -0
- package/template/apps/shared/ui.tsx +128 -0
- package/template/apps/superadmin/src/App.tsx +1264 -0
- package/template/apps/superadmin/src/lib/api.ts +20 -0
- package/template/apps/superadmin/src/lib/hooks.ts +20 -0
- package/template/apps/superadmin/src/main.tsx +16 -0
- package/template/apps/superadmin/src/pages/ClientsPage.tsx +185 -0
- package/template/apps/superadmin/src/pages/InvoicesPage.tsx +157 -0
- package/template/apps/superadmin/src/pages/NewsletterPage.tsx +212 -0
- package/template/apps/superadmin/src/pages/OrdersPage.tsx +195 -0
- package/template/apps/superadmin/src/pages/PaymentsPage.tsx +150 -0
- package/template/apps/superadmin/src/pages/PayoutsPage.tsx +133 -0
- package/template/apps/superadmin/src/pages/PublishersPage.tsx +151 -0
- package/template/apps/superadmin/src/pages/SeoControlPage.tsx +692 -0
- package/template/apps/superadmin/src/pages/SettingsPage.tsx +199 -0
- package/template/apps/superadmin/src/pages/SystemPage.tsx +109 -0
- package/template/apps/superadmin/src/pages/TicketsPage.tsx +201 -0
- package/template/apps/superadmin/src/pages/UsersPage.tsx +167 -0
- package/template/client.html +21 -0
- package/template/community/hamdiayari/akhbar/README.md +23 -0
- package/template/community/hamdiayari/akhbar/screenshot.jpg +0 -0
- package/template/community/hamdiayari/akhbar/snapify.theme.json +76 -0
- package/template/community/hamdiayari/akhbar/src/Brand.tsx +26 -0
- package/template/community/hamdiayari/akhbar/src/Footer.tsx +182 -0
- package/template/community/hamdiayari/akhbar/src/Header.tsx +191 -0
- package/template/community/hamdiayari/akhbar/src/Layout.tsx +49 -0
- package/template/community/hamdiayari/akhbar/src/Social.tsx +33 -0
- package/template/community/hamdiayari/akhbar/src/Ticker.tsx +86 -0
- package/template/community/hamdiayari/akhbar/src/Views.tsx +285 -0
- package/template/community/hamdiayari/akhbar/src/blocks/CollectionBlocks.tsx +672 -0
- package/template/community/hamdiayari/akhbar/src/blocks/ContentBlocks.tsx +305 -0
- package/template/community/hamdiayari/akhbar/src/blocks/ConversionBlocks.tsx +569 -0
- package/template/community/hamdiayari/akhbar/src/blocks/HeaderBlocks.tsx +287 -0
- package/template/community/hamdiayari/akhbar/src/index.tsx +51 -0
- package/template/community/hamdiayari/akhbar/src/kit.tsx +357 -0
- package/template/community/hamdiayari/akhbar/src/site.ts +96 -0
- package/template/community/hamdiayari/akhbar/src/styles.css +566 -0
- package/template/community/hamdiayari/akhbar-actualites/README.md +28 -0
- package/template/community/hamdiayari/akhbar-actualites/screenshot.jpg +0 -0
- package/template/community/hamdiayari/akhbar-actualites/snapify.module.json +107 -0
- package/template/community/hamdiayari/akhbar-actualites/src/admin/AudiencePage.tsx +185 -0
- package/template/community/hamdiayari/akhbar-actualites/src/admin/index.tsx +11 -0
- package/template/community/hamdiayari/akhbar-actualites/src/public/Archive.tsx +142 -0
- package/template/community/hamdiayari/akhbar-actualites/src/public/cards.tsx +152 -0
- package/template/community/hamdiayari/akhbar-actualites/src/public/index.tsx +71 -0
- package/template/community/hamdiayari/akhbar-actualites/src/public/posts.ts +103 -0
- package/template/community/hamdiayari/akhbar-actualites/src/public/styles.css +568 -0
- package/template/community/hamdiayari/akhbar-actualites/src/public/widgets.tsx +226 -0
- package/template/community/hamdiayari/akhbar-actualites/src/server.ts +119 -0
- package/template/community/hamdiayari/akhbar-actualites/src/types.ts +25 -0
- package/template/community/hamdiayari/akhbar-urgent/README.md +36 -0
- package/template/community/hamdiayari/akhbar-urgent/screenshot.jpg +0 -0
- package/template/community/hamdiayari/akhbar-urgent/snapify.module.json +64 -0
- package/template/community/hamdiayari/akhbar-urgent/src/admin/FlashesPage.tsx +229 -0
- package/template/community/hamdiayari/akhbar-urgent/src/admin/index.tsx +11 -0
- package/template/community/hamdiayari/akhbar-urgent/src/public/Ticker.tsx +81 -0
- package/template/community/hamdiayari/akhbar-urgent/src/public/index.tsx +63 -0
- package/template/community/hamdiayari/akhbar-urgent/src/public/styles.css +142 -0
- package/template/community/hamdiayari/akhbar-urgent/src/server.ts +101 -0
- package/template/community/hamdiayari/akhbar-urgent/src/types.ts +35 -0
- package/template/community.html +21 -0
- package/template/developers.html +24 -0
- package/template/docker-compose.db.yml +24 -0
- package/template/docs/BACKOFFICE_SNAPIFY.md +130 -0
- package/template/docs/CI_CD.md +66 -0
- package/template/docs/MODULES.md +346 -0
- package/template/docs/PLATFORM_DEPLOY.md +54 -0
- package/template/docs/logo.svg +10 -0
- package/template/drizzle/0000_init.sql +147 -0
- package/template/drizzle/0001_page_project.sql +5 -0
- package/template/drizzle/0002_modules.sql +23 -0
- package/template/drizzle/0003_themes.sql +4 -0
- package/template/drizzle/0004_security.sql +82 -0
- package/template/drizzle/0005_module_records.sql +16 -0
- package/template/drizzle/0006_avatars.sql +4 -0
- package/template/drizzle/0007_platform.sql +264 -0
- package/template/drizzle/0008_mp_validation.sql +3 -0
- package/template/drizzle/0009_saas_ops.sql +77 -0
- package/template/drizzle/0010_cms_chrome.sql +13 -0
- package/template/drizzle/0011_client_portal.sql +26 -0
- package/template/drizzle/meta/0000_snapshot.json +998 -0
- package/template/drizzle/meta/0001_snapshot.json +1004 -0
- package/template/drizzle/meta/_journal.json +90 -0
- package/template/drizzle.config.ts +16 -0
- package/template/ecosystem.snapify.cjs +24 -0
- package/template/for.html +1 -0
- package/template/index.html +29 -0
- package/template/landing.html +37 -0
- package/template/marketplace/catalog.json +373 -0
- package/template/marketplace/gallery/hamdiayari.preveris/01-home.jpg +0 -0
- package/template/marketplace/gallery/hamdiayari.recaptcha/01-reglages.jpg +0 -0
- package/template/marketplace/gallery/hamdiayari.recaptcha/02-logo.jpg +0 -0
- package/template/marketplace/gallery/novalabs.ai-articles/01-generer.jpg +0 -0
- package/template/marketplace/gallery/novalabs.ai-articles/02-fournisseur-ia.jpg +0 -0
- package/template/marketplace/gallery/novalabs.calendar/01-agenda-site.jpg +0 -0
- package/template/marketplace/gallery/novalabs.calendar/02-evenements-admin.jpg +0 -0
- package/template/marketplace/gallery/novalabs.calendar/03-reglages.jpg +0 -0
- package/template/marketplace/screenshots/hamdiayari.preveris-1.0.6.jpg +0 -0
- package/template/marketplace/screenshots/hamdiayari.recaptcha-1.0.0.jpg +0 -0
- package/template/marketplace/screenshots/hamdiayari.wp-seo-migrator-1.1.1.jpg +0 -0
- package/template/marketplace/screenshots/novalabs.ai-articles-1.0.1.jpg +0 -0
- package/template/marketplace/screenshots/novalabs.ai-chat-1.0.1.jpg +0 -0
- package/template/marketplace/screenshots/novalabs.calendar-1.0.1.jpg +0 -0
- package/template/marketplace/screenshots/novalabs.cart-1.0.0.jpg +0 -0
- package/template/marketplace/screenshots/novalabs.checkout-1.0.0.jpg +0 -0
- package/template/marketplace/screenshots/novalabs.customers-1.1.0.jpg +0 -0
- package/template/marketplace/screenshots/novalabs.dogga-1.0.0.jpg +0 -0
- package/template/marketplace/screenshots/novalabs.mahdia-1.0.0.jpg +0 -0
- package/template/marketplace/screenshots/novalabs.shop-1.0.0.jpg +0 -0
- package/template/marketplace/screenshots/novalabs.sidibou-1.0.0.jpg +0 -0
- package/template/marketplace/screenshots/novalabs.whatsapp-1.0.1.jpg +0 -0
- package/template/marketplace.html +21 -0
- package/template/metadata.json +7 -0
- package/template/modules/ai-articles/screenshot.jpg +0 -0
- package/template/modules/ai-articles/snapify.module.json +74 -0
- package/template/modules/ai-articles/src/admin/ArticlesPage.tsx +162 -0
- package/template/modules/ai-articles/src/admin/GeneratePage.tsx +118 -0
- package/template/modules/ai-articles/src/admin/index.tsx +12 -0
- package/template/modules/ai-articles/src/public/index.tsx +119 -0
- package/template/modules/ai-articles/src/server.ts +287 -0
- package/template/modules/ai-articles/src/types.ts +30 -0
- package/template/modules/ai-chat/screenshot.jpg +0 -0
- package/template/modules/ai-chat/snapify.module.json +217 -0
- package/template/modules/ai-chat/src/public.tsx +198 -0
- package/template/modules/ai-chat/src/server.ts +142 -0
- package/template/modules/ai-theme-studio/README.md +15 -0
- package/template/modules/ai-theme-studio/screenshot.jpg +0 -0
- package/template/modules/ai-theme-studio/snapify.module.json +175 -0
- package/template/modules/ai-theme-studio/src/admin/HistoryPage.tsx +91 -0
- package/template/modules/ai-theme-studio/src/admin/StudioPage.tsx +414 -0
- package/template/modules/ai-theme-studio/src/admin/index.tsx +11 -0
- package/template/modules/ai-theme-studio/src/lib/images.ts +240 -0
- package/template/modules/ai-theme-studio/src/lib/llm.ts +123 -0
- package/template/modules/ai-theme-studio/src/lib/normalize.ts +714 -0
- package/template/modules/ai-theme-studio/src/lib/prompts.ts +198 -0
- package/template/modules/ai-theme-studio/src/server.ts +137 -0
- package/template/modules/ai-theme-studio/src/types.ts +112 -0
- package/template/modules/calendar/screenshot.jpg +0 -0
- package/template/modules/calendar/snapify.module.json +45 -0
- package/template/modules/calendar/src/admin/EventsPage.tsx +217 -0
- package/template/modules/calendar/src/admin/index.tsx +11 -0
- package/template/modules/calendar/src/public/index.tsx +231 -0
- package/template/modules/calendar/src/server.ts +143 -0
- package/template/modules/calendar/src/types.ts +25 -0
- package/template/modules/cart/screenshot.jpg +0 -0
- package/template/modules/cart/snapify.module.json +90 -0
- package/template/modules/cart/src/admin/index.tsx +265 -0
- package/template/modules/cart/src/public/CartPage.tsx +198 -0
- package/template/modules/cart/src/public/index.tsx +261 -0
- package/template/modules/cart/src/public/useQuote.ts +67 -0
- package/template/modules/cart/src/server.ts +148 -0
- package/template/modules/cart/src/types.ts +31 -0
- package/template/modules/checkout/screenshot.jpg +0 -0
- package/template/modules/checkout/snapify.module.json +264 -0
- package/template/modules/checkout/src/admin/OrderDetailPage.tsx +304 -0
- package/template/modules/checkout/src/admin/badges.tsx +28 -0
- package/template/modules/checkout/src/admin/index.tsx +236 -0
- package/template/modules/checkout/src/public/CheckoutPage.tsx +461 -0
- package/template/modules/checkout/src/public/OrderPages.tsx +153 -0
- package/template/modules/checkout/src/public/index.tsx +23 -0
- package/template/modules/checkout/src/server.ts +383 -0
- package/template/modules/checkout/src/types.ts +70 -0
- package/template/modules/customers/screenshot.jpg +0 -0
- package/template/modules/customers/snapify.module.json +115 -0
- package/template/modules/customers/src/admin/CustomerDetailPage.tsx +274 -0
- package/template/modules/customers/src/admin/index.tsx +194 -0
- package/template/modules/customers/src/public/AccountPages.tsx +470 -0
- package/template/modules/customers/src/public/AddressForm.tsx +115 -0
- package/template/modules/customers/src/public/AuthPages.tsx +349 -0
- package/template/modules/customers/src/public/Avatar.tsx +29 -0
- package/template/modules/customers/src/public/ProfileSection.tsx +291 -0
- package/template/modules/customers/src/public/index.tsx +137 -0
- package/template/modules/customers/src/public/session.ts +51 -0
- package/template/modules/customers/src/server.ts +421 -0
- package/template/modules/customers/src/types.ts +53 -0
- package/template/modules/recaptcha/logo.jpg +0 -0
- package/template/modules/recaptcha/screenshot.jpg +0 -0
- package/template/modules/recaptcha/snapify.module.json +99 -0
- package/template/modules/recaptcha/src/public/index.tsx +94 -0
- package/template/modules/recaptcha/src/server.ts +65 -0
- package/template/modules/recaptcha/src/types.ts +18 -0
- package/template/modules/shop/screenshot.jpg +0 -0
- package/template/modules/shop/snapify.module.json +340 -0
- package/template/modules/shop/src/admin/CategoriesPage.tsx +224 -0
- package/template/modules/shop/src/admin/ProductEditor.tsx +313 -0
- package/template/modules/shop/src/admin/ProductsPage.tsx +307 -0
- package/template/modules/shop/src/admin/index.tsx +14 -0
- package/template/modules/shop/src/public/Catalog.tsx +244 -0
- package/template/modules/shop/src/public/NotFound.tsx +22 -0
- package/template/modules/shop/src/public/ProductPage.tsx +195 -0
- package/template/modules/shop/src/public/index.tsx +134 -0
- package/template/modules/shop/src/public/useFetch.ts +26 -0
- package/template/modules/shop/src/server.ts +493 -0
- package/template/modules/shop/src/shared.ts +41 -0
- package/template/modules/shop/src/types.ts +60 -0
- package/template/modules/whatsapp/screenshot.jpg +0 -0
- package/template/modules/whatsapp/snapify.module.json +121 -0
- package/template/modules/whatsapp/src/public.tsx +119 -0
- package/template/modules/wp-seo-migrator/ARCHITECTURE.md +113 -0
- package/template/modules/wp-seo-migrator/README.md +21 -0
- package/template/modules/wp-seo-migrator/fixtures/aioseo-sample.csv +3 -0
- package/template/modules/wp-seo-migrator/fixtures/sample-exporter.json +39 -0
- package/template/modules/wp-seo-migrator/fixtures/sample-page.html +12 -0
- package/template/modules/wp-seo-migrator/fixtures/sample.htaccess +3 -0
- package/template/modules/wp-seo-migrator/screenshot.jpg +0 -0
- package/template/modules/wp-seo-migrator/snapify.module.json +87 -0
- package/template/modules/wp-seo-migrator/src/admin/HistoryPage.tsx +64 -0
- package/template/modules/wp-seo-migrator/src/admin/WizardPage.tsx +788 -0
- package/template/modules/wp-seo-migrator/src/admin/i18n.ts +156 -0
- package/template/modules/wp-seo-migrator/src/admin/index.tsx +11 -0
- package/template/modules/wp-seo-migrator/src/connectors/CUSTOM_CONNECTOR.md +31 -0
- package/template/modules/wp-seo-migrator/src/connectors/csvSeoFactory.ts +106 -0
- package/template/modules/wp-seo-migrator/src/connectors/exporter.ts +96 -0
- package/template/modules/wp-seo-migrator/src/connectors/gscCsv.ts +53 -0
- package/template/modules/wp-seo-migrator/src/connectors/htaccess.ts +65 -0
- package/template/modules/wp-seo-migrator/src/connectors/htmlCrawler.ts +90 -0
- package/template/modules/wp-seo-migrator/src/connectors/index.ts +113 -0
- package/template/modules/wp-seo-migrator/src/connectors/pluginCsv.ts +77 -0
- package/template/modules/wp-seo-migrator/src/connectors/rankMathCsv.ts +77 -0
- package/template/modules/wp-seo-migrator/src/connectors/redirection.ts +62 -0
- package/template/modules/wp-seo-migrator/src/connectors/sitemap.ts +44 -0
- package/template/modules/wp-seo-migrator/src/connectors/utils.ts +111 -0
- package/template/modules/wp-seo-migrator/src/connectors/wxr.ts +103 -0
- package/template/modules/wp-seo-migrator/src/connectors/yoastCsv.ts +56 -0
- package/template/modules/wp-seo-migrator/src/pipeline/analyze.ts +158 -0
- package/template/modules/wp-seo-migrator/src/pipeline/htmlParse.ts +58 -0
- package/template/modules/wp-seo-migrator/src/pipeline/remoteAuto.ts +328 -0
- package/template/modules/wp-seo-migrator/src/server.ts +492 -0
- package/template/modules/wp-seo-migrator/src/types.ts +151 -0
- package/template/modules/wp-seo-migrator/tests/connectors-extra.test.ts +39 -0
- package/template/modules/wp-seo-migrator/tests/merge.test.ts +25 -0
- package/template/package-lock.json +6016 -0
- package/template/package.json +74 -0
- package/template/public/images/photos/caravane-chameaux-desert.webp +0 -0
- package/template/public/images/photos/hammamet-medina-plage.jpg +0 -0
- package/template/public/images/photos/maison-campagne-collines.webp +0 -0
- package/template/public/images/photos/maisons-troglodytes-desert.jpg +0 -0
- package/template/public/images/photos/panorama-ville.jpg +0 -0
- package/template/public/images/photos/preveris-accessibilite-erp.jpg +0 -0
- package/template/public/images/photos/preveris-collectivites.jpg +0 -0
- package/template/public/images/photos/preveris-etudes-plans.jpg +0 -0
- package/template/public/images/photos/preveris-hero-erp.jpg +0 -0
- package/template/public/images/photos/preveris-ssi-incendie.jpg +0 -0
- package/template/public/images/photos/preveris-suivi-hotel.jpg +0 -0
- package/template/public/images/photos/ruelle-bleue-medina.jpg +0 -0
- package/template/public/images/photos/sidi-bou-said-port.webp +0 -0
- package/template/public/images/photos/terrasse-blanche-mer-voilier.jpg +0 -0
- package/template/public/landing/hero-ai-cms.jpg +0 -0
- package/template/public/landing/shot-dogga.jpg +0 -0
- package/template/public/landing/shot-mahdia.jpg +0 -0
- package/template/public/landing/shot-sidibou.jpg +0 -0
- package/template/public/llms.txt +35 -0
- package/template/public/logo.svg +10 -0
- package/template/scripts/app-pack.ts +155 -0
- package/template/scripts/app-publish.ts +123 -0
- package/template/scripts/apply-ecommerce-home.ts +196 -0
- package/template/scripts/auto-deploy-poll.sh +13 -0
- package/template/scripts/db-backup.sh +16 -0
- package/template/scripts/db-restore.sh +11 -0
- package/template/scripts/deploy.sh +34 -0
- package/template/scripts/github-webhook-deploy.sh +7 -0
- package/template/scripts/install-new-modules.ts +96 -0
- package/template/scripts/refresh-demo-baseline.ts +9 -0
- package/template/scripts/reinstall-marketplace-packs.ts +51 -0
- package/template/scripts/restore-classic-home.ts +13 -0
- package/template/scripts/sdk-pack.ts +289 -0
- package/template/scripts/sdk-publish.ts +75 -0
- package/template/scripts/seed-demo-products.ts +120 -0
- package/template/scripts/seed-theme-content-packs.ts +42 -0
- package/template/scripts/snapify-module.ts +207 -0
- package/template/scripts/test-theme-switch.ts +24 -0
- package/template/sdk/admin.css +18 -0
- package/template/sdk/admin.ts +95 -0
- package/template/sdk/build.ts +249 -0
- package/template/sdk/commerce.ts +512 -0
- package/template/sdk/content.ts +76 -0
- package/template/sdk/icons.ts +101 -0
- package/template/sdk/image.ts +60 -0
- package/template/sdk/index.ts +500 -0
- package/template/sdk/package.ts +298 -0
- package/template/sdk/recaptcha.tsx +122 -0
- package/template/sdk/settings.ts +84 -0
- package/template/sdk/theme.css +29 -0
- package/template/sdk/theme.ts +100 -0
- package/template/sdk/ui/commerce.tsx +385 -0
- package/template/sdk/ui/orders.tsx +136 -0
- package/template/sdk-kit/CHANGELOG.md +17 -0
- package/template/sdk-kit/LICENSE.md +47 -0
- package/template/sdk-kit/README.md +81 -0
- package/template/sdk-kit/cli.ts +339 -0
- package/template/sdk-kit/create-snapify/README.md +28 -0
- package/template/sdk-kit/create-snapify/create-snapify.mjs +11 -0
- package/template/sdk-kit/create-snapify/package.template.json +16 -0
- package/template/sdk-kit/package.template.json +95 -0
- package/template/sdk-kit/templates/common/README.md.tpl +27 -0
- package/template/sdk-kit/templates/common/_gitignore +3 -0
- package/template/sdk-kit/templates/common/package.json.tpl +19 -0
- package/template/sdk-kit/templates/common/src/snapify-env.d.ts +2 -0
- package/template/sdk-kit/templates/common/tsconfig.json +15 -0
- package/template/sdk-kit/templates/module/snapify.module.json.tpl +33 -0
- package/template/sdk-kit/templates/module/src/admin.tsx +53 -0
- package/template/sdk-kit/templates/module/src/public.tsx +49 -0
- package/template/sdk-kit/templates/module/src/server.ts +30 -0
- package/template/sdk-kit/templates/module/src/types.ts +19 -0
- package/template/sdk-kit/templates/theme/snapify.theme.json.tpl +22 -0
- package/template/sdk-kit/templates/theme/src/Footer.tsx +40 -0
- package/template/sdk-kit/templates/theme/src/Header.tsx +47 -0
- package/template/sdk-kit/templates/theme/src/Views.tsx +36 -0
- package/template/sdk-kit/templates/theme/src/blocks.tsx +93 -0
- package/template/sdk-kit/templates/theme/src/index.tsx +30 -0
- package/template/sdk-kit/templates/theme/src/site.tsx +59 -0
- package/template/sdk-kit/templates/theme/src/styles.css +71 -0
- package/template/server/client/routes.ts +252 -0
- package/template/server/cms/analyticsStore.ts +180 -0
- package/template/server/cms/auth.ts +155 -0
- package/template/server/cms/blockCatalog.ts +342 -0
- package/template/server/cms/blocks.ts +521 -0
- package/template/server/cms/contentMigration.ts +1119 -0
- package/template/server/cms/defaults.ts +303 -0
- package/template/server/cms/permissions.ts +154 -0
- package/template/server/cms/restoreClassicHome.ts +183 -0
- package/template/server/cms/revisions.ts +99 -0
- package/template/server/cms/scheduler.ts +78 -0
- package/template/server/cms/seed.ts +72 -0
- package/template/server/cms/seoAnalyzer.ts +213 -0
- package/template/server/cms/store.ts +122 -0
- package/template/server/cms/themeContent.ts +1386 -0
- package/template/server/cms/themePacks/sidibou-branding.json +1 -0
- package/template/server/cms/themePacks/sidibou-home.json +1 -0
- package/template/server/cms/themePacks/sidibou-menus.json +1 -0
- package/template/server/cms/types.ts +432 -0
- package/template/server/community/routes.ts +410 -0
- package/template/server/db/client.ts +43 -0
- package/template/server/db/repository.ts +389 -0
- package/template/server/db/schema.ts +726 -0
- package/template/server/demo/cmsReset.ts +196 -0
- package/template/server/developers/sdk.ts +85 -0
- package/template/server/geoip.ts +15 -0
- package/template/server/http.ts +53 -0
- package/template/server/mail.ts +216 -0
- package/template/server/marketplace/catalog.ts +74 -0
- package/template/server/marketplace/packageValidate.ts +475 -0
- package/template/server/marketplace/payments.ts +193 -0
- package/template/server/marketplace/routes.ts +850 -0
- package/template/server/marketplace/signing.ts +116 -0
- package/template/server/marketplace/syncOfficial.ts +282 -0
- package/template/server/media/images.ts +63 -0
- package/template/server/middleware/adminAuth.ts +134 -0
- package/template/server/modules/legacy.ts +44 -0
- package/template/server/modules/manager.ts +1122 -0
- package/template/server/modules/marketplace.ts +178 -0
- package/template/server/modules/outbound.ts +178 -0
- package/template/server/modules/purchases.ts +149 -0
- package/template/server/modules/records.ts +168 -0
- package/template/server/modules/themeHtml.ts +32 -0
- package/template/server/modules/trust.ts +16 -0
- package/template/server/modules/types.ts +147 -0
- package/template/server/platform/auth.ts +223 -0
- package/template/server/platform/clientWelcome.ts +168 -0
- package/template/server/platform/clients.ts +139 -0
- package/template/server/platform/dailyImprove.ts +356 -0
- package/template/server/platform/flouci.ts +126 -0
- package/template/server/platform/host.ts +69 -0
- package/template/server/platform/hostingProviders.ts +143 -0
- package/template/server/platform/licenseOrders.ts +291 -0
- package/template/server/platform/money.ts +42 -0
- package/template/server/platform/passwords.ts +41 -0
- package/template/server/platform/region.ts +86 -0
- package/template/server/platform/routes.ts +225 -0
- package/template/server/platform/seed.ts +82 -0
- package/template/server/platform/settings.ts +72 -0
- package/template/server/routes/adminAccount.ts +403 -0
- package/template/server/routes/adminCms.ts +808 -0
- package/template/server/routes/adminSystem.ts +243 -0
- package/template/server/routes/adminUsers.ts +449 -0
- package/template/server/routes/googleOAuth.ts +327 -0
- package/template/server/routes/modules.ts +323 -0
- package/template/server/routes/publicCms.ts +313 -0
- package/template/server/routes/seoPublic.ts +158 -0
- package/template/server/security/accounts.ts +121 -0
- package/template/server/security/audit.ts +143 -0
- package/template/server/security/authTokens.ts +74 -0
- package/template/server/security/ip.ts +68 -0
- package/template/server/security/rateLimit.ts +85 -0
- package/template/server/security/retention.ts +46 -0
- package/template/server/security/secrets.ts +44 -0
- package/template/server/security/sessions.ts +140 -0
- package/template/server/security/totp.ts +101 -0
- package/template/server/superadmin/analytics.ts +109 -0
- package/template/server/superadmin/auth.ts +92 -0
- package/template/server/superadmin/backlinksCatalog.ts +210 -0
- package/template/server/superadmin/frSeoContent.ts +165 -0
- package/template/server/superadmin/landingSeo.ts +675 -0
- package/template/server/superadmin/ops.ts +238 -0
- package/template/server/superadmin/routes.ts +1095 -0
- package/template/server/superadmin/tnSeoContent.ts +130 -0
- package/template/server.ts +522 -0
- package/template/src/App.tsx +263 -0
- package/template/src/admin/AdminApp.tsx +162 -0
- package/template/src/admin/account/AuthLayout.tsx +57 -0
- package/template/src/admin/account/SecurityForms.tsx +276 -0
- package/template/src/admin/admin.css +127 -0
- package/template/src/admin/context/AuthContext.tsx +147 -0
- package/template/src/admin/context/SiteContext.tsx +50 -0
- package/template/src/admin/editor/BlockLibrary.tsx +173 -0
- package/template/src/admin/editor/BlocksPanel.tsx +396 -0
- package/template/src/admin/editor/EditorBits.tsx +106 -0
- package/template/src/admin/editor/PreviewFrame.tsx +145 -0
- package/template/src/admin/editor/RichTextEditor.tsx +276 -0
- package/template/src/admin/editor/SeoPanel.tsx +349 -0
- package/template/src/admin/editor/StyleEditor.tsx +114 -0
- package/template/src/admin/editor/blockSchemas.ts +699 -0
- package/template/src/admin/editor/fields.tsx +486 -0
- package/template/src/admin/layout/AdminShell.tsx +418 -0
- package/template/src/admin/layout/nav.ts +95 -0
- package/template/src/admin/lib/api.ts +408 -0
- package/template/src/admin/lib/audit.ts +141 -0
- package/template/src/admin/lib/format.ts +84 -0
- package/template/src/admin/lib/hooks.ts +125 -0
- package/template/src/admin/main.tsx +16 -0
- package/template/src/admin/modules/InstallDialog.tsx +160 -0
- package/template/src/admin/modules/SettingsForm.tsx +145 -0
- package/template/src/admin/modules/kit.tsx +127 -0
- package/template/src/admin/modules/labels.tsx +226 -0
- package/template/src/admin/pages/AccountSetupPage.tsx +62 -0
- package/template/src/admin/pages/AnalyticsPage.tsx +206 -0
- package/template/src/admin/pages/BrandingPage.tsx +154 -0
- package/template/src/admin/pages/DashboardPage.tsx +239 -0
- package/template/src/admin/pages/EngagementPage.tsx +168 -0
- package/template/src/admin/pages/FormsPage.tsx +435 -0
- package/template/src/admin/pages/GeneralPage.tsx +120 -0
- package/template/src/admin/pages/LoginPage.tsx +249 -0
- package/template/src/admin/pages/MaintenancePage.tsx +125 -0
- package/template/src/admin/pages/MarketplacePage.tsx +426 -0
- package/template/src/admin/pages/MediaPage.tsx +251 -0
- package/template/src/admin/pages/MenusPage.tsx +446 -0
- package/template/src/admin/pages/ModuleAdminPage.tsx +113 -0
- package/template/src/admin/pages/ModuleDetailPage.tsx +293 -0
- package/template/src/admin/pages/ModulesPage.tsx +135 -0
- package/template/src/admin/pages/NewsletterPage.tsx +193 -0
- package/template/src/admin/pages/PageEditorPage.tsx +322 -0
- package/template/src/admin/pages/PagesListPage.tsx +277 -0
- package/template/src/admin/pages/PostEditorPage.tsx +292 -0
- package/template/src/admin/pages/PostsListPage.tsx +187 -0
- package/template/src/admin/pages/ProfilePage.tsx +489 -0
- package/template/src/admin/pages/ResetPasswordPage.tsx +110 -0
- package/template/src/admin/pages/SeoPage.tsx +670 -0
- package/template/src/admin/pages/SettingsPage.tsx +189 -0
- package/template/src/admin/pages/ThemeCustomizePage.tsx +170 -0
- package/template/src/admin/pages/ThemesPage.tsx +292 -0
- package/template/src/admin/pages/UsersPage.tsx +57 -0
- package/template/src/admin/pages/WidgetsPage.tsx +200 -0
- package/template/src/admin/seo/GoogleSetup.tsx +529 -0
- package/template/src/admin/settings/EmailTab.tsx +293 -0
- package/template/src/admin/settings/OtherTabs.tsx +270 -0
- package/template/src/admin/settings/SecurityTab.tsx +172 -0
- package/template/src/admin/settings/controls.tsx +133 -0
- package/template/src/admin/ui/Pagination.tsx +170 -0
- package/template/src/admin/ui/SaveBar.tsx +77 -0
- package/template/src/admin/ui/TagInput.tsx +54 -0
- package/template/src/admin/ui/charts.tsx +108 -0
- package/template/src/admin/ui/media.tsx +265 -0
- package/template/src/admin/ui/overlays.tsx +239 -0
- package/template/src/admin/ui/primitives.tsx +544 -0
- package/template/src/admin/users/AuditTab.tsx +104 -0
- package/template/src/admin/users/RolesTab.tsx +368 -0
- package/template/src/admin/users/UsersTab.tsx +723 -0
- package/template/src/admin/users/shared.tsx +278 -0
- package/template/src/cms/icons.ts +7 -0
- package/template/src/components/CookieBanner.tsx +166 -0
- package/template/src/components/MaintenanceGate.tsx +66 -0
- package/template/src/components/SiteAnalytics.tsx +64 -0
- package/template/src/components/SnapifyLogo.tsx +44 -0
- package/template/src/components/cms/BlockRenderer.tsx +154 -0
- package/template/src/components/cms/WidgetArea.tsx +193 -0
- package/template/src/context/SiteConfigContext.tsx +130 -0
- package/template/src/demoBase.ts +23 -0
- package/template/src/index.css +90 -0
- package/template/src/main.tsx +16 -0
- package/template/src/modules/ModuleHost.tsx +197 -0
- package/template/src/preview.ts +63 -0
- package/template/src/runtime.ts +38 -0
- package/template/src/themes/ThemeHost.tsx +168 -0
- package/template/src/themes/fallback.tsx +71 -0
- package/template/src/types.ts +94 -0
- package/template/src/views/BlogPostView.tsx +68 -0
- package/template/src/views/CmsPageView.tsx +34 -0
- package/template/start-pm2.sh +3 -0
- package/template/superadmin.html +20 -0
- package/template/themes/ai-canvas/screenshot.jpg +0 -0
- package/template/themes/ai-canvas/snapify.theme.json +96 -0
- package/template/themes/ai-canvas/src/CeeSimulator.tsx +247 -0
- package/template/themes/ai-canvas/src/Footer.tsx +190 -0
- package/template/themes/ai-canvas/src/Header.tsx +324 -0
- package/template/themes/ai-canvas/src/Layout.tsx +56 -0
- package/template/themes/ai-canvas/src/NotFound.tsx +58 -0
- package/template/themes/ai-canvas/src/Post.tsx +26 -0
- package/template/themes/ai-canvas/src/SnapifyLogo.tsx +44 -0
- package/template/themes/ai-canvas/src/blocks/CollectionBlocks.tsx +711 -0
- package/template/themes/ai-canvas/src/blocks/ContentBlocks.tsx +318 -0
- package/template/themes/ai-canvas/src/blocks/ConversionBlocks.tsx +559 -0
- package/template/themes/ai-canvas/src/blocks/HeaderBlocks.tsx +341 -0
- package/template/themes/ai-canvas/src/index.tsx +54 -0
- package/template/themes/ai-canvas/src/kit.tsx +398 -0
- package/template/themes/ai-canvas/src/site.ts +29 -0
- package/template/themes/ai-canvas/src/styles.css +317 -0
- package/template/themes/dogga/screenshot.jpg +0 -0
- package/template/themes/dogga/snapify.theme.json +55 -0
- package/template/themes/dogga/src/Brand.tsx +41 -0
- package/template/themes/dogga/src/Footer.tsx +202 -0
- package/template/themes/dogga/src/Header.tsx +239 -0
- package/template/themes/dogga/src/Layout.tsx +43 -0
- package/template/themes/dogga/src/Views.tsx +71 -0
- package/template/themes/dogga/src/blocks/CollectionBlocks.tsx +674 -0
- package/template/themes/dogga/src/blocks/ContentBlocks.tsx +307 -0
- package/template/themes/dogga/src/blocks/ConversionBlocks.tsx +571 -0
- package/template/themes/dogga/src/blocks/HeaderBlocks.tsx +289 -0
- package/template/themes/dogga/src/index.tsx +53 -0
- package/template/themes/dogga/src/kit.tsx +362 -0
- package/template/themes/dogga/src/site.ts +56 -0
- package/template/themes/dogga/src/styles.css +396 -0
- package/template/themes/mahdia/screenshot.jpg +0 -0
- package/template/themes/mahdia/snapify.theme.json +65 -0
- package/template/themes/mahdia/src/Brand.tsx +39 -0
- package/template/themes/mahdia/src/Footer.tsx +210 -0
- package/template/themes/mahdia/src/Header.tsx +291 -0
- package/template/themes/mahdia/src/Layout.tsx +44 -0
- package/template/themes/mahdia/src/Views.tsx +79 -0
- package/template/themes/mahdia/src/blocks/CollectionBlocks.tsx +674 -0
- package/template/themes/mahdia/src/blocks/ContentBlocks.tsx +307 -0
- package/template/themes/mahdia/src/blocks/ConversionBlocks.tsx +571 -0
- package/template/themes/mahdia/src/blocks/HeaderBlocks.tsx +289 -0
- package/template/themes/mahdia/src/index.tsx +53 -0
- package/template/themes/mahdia/src/kit.tsx +362 -0
- package/template/themes/mahdia/src/site.ts +75 -0
- package/template/themes/mahdia/src/styles.css +446 -0
- package/template/themes/preveris/screenshot.jpg +0 -0
- package/template/themes/preveris/snapify.theme.json +126 -0
- package/template/themes/preveris/src/Brand.tsx +44 -0
- package/template/themes/preveris/src/Footer.tsx +202 -0
- package/template/themes/preveris/src/Header.tsx +239 -0
- package/template/themes/preveris/src/Layout.tsx +42 -0
- package/template/themes/preveris/src/Views.tsx +71 -0
- package/template/themes/preveris/src/blocks/CollectionBlocks.tsx +678 -0
- package/template/themes/preveris/src/blocks/ContentBlocks.tsx +307 -0
- package/template/themes/preveris/src/blocks/ConversionBlocks.tsx +571 -0
- package/template/themes/preveris/src/blocks/HeaderBlocks.tsx +299 -0
- package/template/themes/preveris/src/index.tsx +53 -0
- package/template/themes/preveris/src/kit.tsx +365 -0
- package/template/themes/preveris/src/site.ts +79 -0
- package/template/themes/preveris/src/styles.css +446 -0
- package/template/themes/sidibou/screenshot.jpg +0 -0
- package/template/themes/sidibou/snapify.theme.json +21 -0
- package/template/themes/sidibou/src/CeeSimulator.tsx +247 -0
- package/template/themes/sidibou/src/Footer.tsx +203 -0
- package/template/themes/sidibou/src/Header.tsx +324 -0
- package/template/themes/sidibou/src/Layout.tsx +53 -0
- package/template/themes/sidibou/src/NotFound.tsx +58 -0
- package/template/themes/sidibou/src/Post.tsx +26 -0
- package/template/themes/sidibou/src/SnapifyLogo.tsx +44 -0
- package/template/themes/sidibou/src/blocks/CollectionBlocks.tsx +711 -0
- package/template/themes/sidibou/src/blocks/ContentBlocks.tsx +318 -0
- package/template/themes/sidibou/src/blocks/ConversionBlocks.tsx +559 -0
- package/template/themes/sidibou/src/blocks/HeaderBlocks.tsx +341 -0
- package/template/themes/sidibou/src/index.tsx +54 -0
- package/template/themes/sidibou/src/kit.tsx +398 -0
- package/template/themes/sidibou/src/site.ts +28 -0
- package/template/themes/sidibou/src/styles.css +317 -0
- package/template/tsconfig.json +33 -0
- package/template/vite.config.ts +42 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapify
|
|
3
|
+
* © 2026 NOVALABS WEB DESIGN — Tunisie
|
|
4
|
+
* Contact : contact@novalabs.tn · +216 24 734 647
|
|
5
|
+
* Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Analyse d’un paquet .snapmod / .snaptheme déposé sur la marketplace : structure, empreintes,
|
|
9
|
+
* signature, licence, compatibilité, puis analyse statique du code (syntaxe, imports, exports,
|
|
10
|
+
* accès système, cohérence permissions ↔ appels `ctx`, styles, réglages, visuels).
|
|
11
|
+
* Le code du paquet n’est jamais exécuté : il est analysé par esbuild et par motifs.
|
|
12
|
+
*/
|
|
13
|
+
import crypto from 'crypto';
|
|
14
|
+
import * as esbuild from 'esbuild';
|
|
15
|
+
import { SNAPIFY_MODULE_API_VERSION, type PackagedManifest } from '../../sdk/index';
|
|
16
|
+
import { checkLicensePayload, compareVersions, decodePackage, PackageError, satisfies, verify, type ModulePackage } from '../../sdk/package';
|
|
17
|
+
import { TRUSTED_KEYS } from '../modules/trust';
|
|
18
|
+
|
|
19
|
+
export type CheckGroup = 'Paquet' | 'Manifeste' | 'Code' | 'Permissions' | 'Sécurité' | 'Présentation' | 'Marketplace';
|
|
20
|
+
|
|
21
|
+
export type ValidationCheck = {
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
status: 'pass' | 'fail' | 'warn';
|
|
25
|
+
detail?: string;
|
|
26
|
+
group?: CheckGroup;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type PackageValidationResult = {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
checks: ValidationCheck[];
|
|
32
|
+
manifest?: ModulePackage['manifest'];
|
|
33
|
+
signed: boolean;
|
|
34
|
+
signatureKid?: string;
|
|
35
|
+
hasLicense: boolean;
|
|
36
|
+
fileCount: number;
|
|
37
|
+
size: number;
|
|
38
|
+
sha256: string;
|
|
39
|
+
errors: string[];
|
|
40
|
+
warnings: string[];
|
|
41
|
+
/** Capacités `ctx` détectées dans server.mjs, pour la revue. */
|
|
42
|
+
analysis?: { capabilities: string[]; envVars: string[]; hosts: string[]; files: { name: string; size: number }[] };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const hashBuf = (buf: Buffer) => crypto.createHash('sha256').update(buf).digest('hex');
|
|
46
|
+
const kb = (n: number) => `${(n / 1024).toFixed(1)} Ko`;
|
|
47
|
+
|
|
48
|
+
class Report {
|
|
49
|
+
checks: ValidationCheck[] = [];
|
|
50
|
+
errors: string[] = [];
|
|
51
|
+
warnings: string[] = [];
|
|
52
|
+
constructor(private group: CheckGroup = 'Paquet') {}
|
|
53
|
+
in(group: CheckGroup) {
|
|
54
|
+
this.group = group;
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
pass(id: string, label: string, detail?: string) {
|
|
58
|
+
this.checks.push({ id, label, status: 'pass', detail, group: this.group });
|
|
59
|
+
}
|
|
60
|
+
warn(id: string, label: string, detail: string) {
|
|
61
|
+
this.checks.push({ id, label, status: 'warn', detail, group: this.group });
|
|
62
|
+
this.warnings.push(`${label} : ${detail}`);
|
|
63
|
+
}
|
|
64
|
+
fail(id: string, label: string, detail: string) {
|
|
65
|
+
this.checks.push({ id, label, status: 'fail', detail, group: this.group });
|
|
66
|
+
this.errors.push(`${label} : ${detail}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ------------------------------------------------------------------ Code */
|
|
71
|
+
|
|
72
|
+
/** Modules Node sans accès au système, déjà utilisés par les modules officiels. */
|
|
73
|
+
const NODE_ALLOWED = new Set(['crypto', 'util', 'url', 'buffer', 'events', 'string_decoder', 'querystring', 'path', 'assert']);
|
|
74
|
+
/** Accès au disque, aux processus, au réseau brut ou au moteur : contournent les permissions. */
|
|
75
|
+
const NODE_FORBIDDEN = new Set([
|
|
76
|
+
'fs', 'fs/promises', 'child_process', 'net', 'tls', 'dgram', 'dns', 'http', 'https', 'http2', 'os', 'cluster', 'worker_threads',
|
|
77
|
+
'vm', 'v8', 'module', 'inspector', 'process', 'repl', 'readline', 'perf_hooks', 'async_hooks', 'diagnostics_channel', 'wasi', 'trace_events',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
type CodeInfo = { imports: { path: string; kind: string }[]; exports: string[] };
|
|
81
|
+
|
|
82
|
+
async function parseBundle(code: string, name: string): Promise<CodeInfo | { error: string }> {
|
|
83
|
+
try {
|
|
84
|
+
const r = await esbuild.build({
|
|
85
|
+
stdin: { contents: code, loader: 'js', sourcefile: name },
|
|
86
|
+
bundle: true,
|
|
87
|
+
write: false,
|
|
88
|
+
metafile: true,
|
|
89
|
+
format: 'esm',
|
|
90
|
+
platform: 'neutral',
|
|
91
|
+
external: ['*'],
|
|
92
|
+
logLevel: 'silent',
|
|
93
|
+
});
|
|
94
|
+
const input = r.metafile.inputs[name];
|
|
95
|
+
const output = Object.values(r.metafile.outputs)[0];
|
|
96
|
+
return { imports: (input?.imports || []).map((i) => ({ path: i.path, kind: i.kind })), exports: output?.exports || [] };
|
|
97
|
+
} catch (e) {
|
|
98
|
+
const errs = (e as { errors?: { text: string; location?: { line: number; column: number } | null }[] }).errors;
|
|
99
|
+
const first = errs?.[0];
|
|
100
|
+
return { error: first ? `${first.text}${first.location ? ` (ligne ${first.location.line}, col. ${first.location.column})` : ''}` : 'syntaxe invalide' };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Capacité `ctx` → permission requise. */
|
|
105
|
+
const CAPABILITIES: { re: RegExp; cap: string; perm: string | ((m: PackagedManifest) => boolean) }[] = [
|
|
106
|
+
{ re: /\.http\.(?:get|post)\(/, cap: 'ctx.http (routes publiques)', perm: 'http:routes' },
|
|
107
|
+
{ re: /\.db\.collection\(/, cap: 'ctx.db (collections)', perm: 'database' },
|
|
108
|
+
{ re: /\.storage\.(?:get|set|delete)\(/, cap: 'ctx.storage', perm: 'storage' },
|
|
109
|
+
{ re: /\.files\.(?:saveImage|delete)\(/, cap: 'ctx.files', perm: 'files' },
|
|
110
|
+
{ re: /\.mail\.send\(/, cap: 'ctx.mail.send', perm: 'mail:send' },
|
|
111
|
+
{ re: /\.site\(\)/, cap: 'ctx.site()', perm: 'site:read' },
|
|
112
|
+
{ re: /\.admin\.(?:get|post|put|delete)\(/, cap: 'ctx.admin (routes d’administration)', perm: 'admin:routes' },
|
|
113
|
+
{ re: /\.fetch\(/, cap: 'ctx.fetch (réseau)', perm: (m) => m.permissions.some((p) => p.startsWith('net:')) },
|
|
114
|
+
{ re: /\.ftp\.(?:list|readText)\(/, cap: 'ctx.ftp', perm: 'net:ftp' },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
function analyzeServer(code: string, m: PackagedManifest, r: Report) {
|
|
118
|
+
const declared = new Set(m.permissions);
|
|
119
|
+
const capabilities: string[] = [];
|
|
120
|
+
const used = new Set<string>();
|
|
121
|
+
|
|
122
|
+
r.in('Permissions');
|
|
123
|
+
for (const c of CAPABILITIES) {
|
|
124
|
+
if (!c.re.test(code)) continue;
|
|
125
|
+
capabilities.push(c.cap);
|
|
126
|
+
const ok = typeof c.perm === 'string' ? declared.has(c.perm as never) : c.perm(m);
|
|
127
|
+
const permLabel = typeof c.perm === 'string' ? c.perm : 'net:<hôte>';
|
|
128
|
+
if (typeof c.perm === 'string') used.add(c.perm);
|
|
129
|
+
if (ok) r.pass(`cap-${permLabel}`, `${c.cap}`, `permission ${permLabel} déclarée`);
|
|
130
|
+
else r.fail(`cap-${permLabel}`, `${c.cap}`, `utilisé sans la permission ${permLabel} : l’appel échouera sur le site`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const envVars = [...new Set([...code.matchAll(/\.env\(\s*["'`]([A-Za-z0-9_]+)["'`]\s*\)/g)].map((x) => x[1]))];
|
|
134
|
+
for (const v of envVars) {
|
|
135
|
+
used.add(`env:${v}`);
|
|
136
|
+
if (declared.has(`env:${v}` as never)) r.pass(`env-${v}`, `Variable ${v}`, `permission env:${v} déclarée`);
|
|
137
|
+
else r.fail(`env-${v}`, `Variable ${v}`, `lue avec ctx.env() sans la permission env:${v}`);
|
|
138
|
+
}
|
|
139
|
+
if (/\.env\(\s*[^"'`\s)]/.test(code)) r.warn('env-dynamic', 'Variables d’environnement', 'nom calculé à l’exécution : seules les variables déclarées dans les permissions seront lisibles');
|
|
140
|
+
|
|
141
|
+
const hostsInCode = [...new Set([...code.matchAll(/https:\/\/([a-z0-9-]+(?:\.[a-z0-9-]+)+)/gi)].map((x) => x[1].toLowerCase()))];
|
|
142
|
+
const netHosts = m.permissions.filter((p) => p.startsWith('net:')).map((p) => p.slice(4));
|
|
143
|
+
for (const h of netHosts) {
|
|
144
|
+
if (hostsInCode.includes(h)) used.add(`net:${h}`);
|
|
145
|
+
}
|
|
146
|
+
const undeclaredHosts = /\.fetch\(/.test(code) ? hostsInCode.filter((h) => !netHosts.includes(h)) : [];
|
|
147
|
+
if (undeclaredHosts.length) {
|
|
148
|
+
r.warn('net-undeclared', 'Adresses externes', `${undeclaredHosts.join(', ')} apparaissent dans le code sans permission net: — ctx.fetch() les refusera`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const unused = m.permissions.filter(
|
|
152
|
+
(p) =>
|
|
153
|
+
!used.has(p) &&
|
|
154
|
+
p !== 'http:cookies' &&
|
|
155
|
+
!(p === 'net:outbound' && /\.fetch\(/.test(code)) &&
|
|
156
|
+
!(p === 'net:ftp' && /\.ftp\.(?:list|readText)\(/.test(code)) &&
|
|
157
|
+
!(p.startsWith('net:') && p !== 'net:outbound' && p !== 'net:ftp' && !hostsInCode.length && /\.fetch\(/.test(code))
|
|
158
|
+
);
|
|
159
|
+
if (unused.length) r.warn('perm-unused', 'Permissions non utilisées', `${unused.join(', ')} : retirez-les si elles ne servent pas, elles sont affichées au client`);
|
|
160
|
+
else if (m.permissions.length) r.pass('perm-unused', 'Permissions minimales', 'toutes les permissions déclarées sont utilisées');
|
|
161
|
+
if (declared.has('http:cookies') && !/\.http\.reply\(/.test(code) && !/cookies/.test(code)) {
|
|
162
|
+
r.warn('perm-cookies', 'Permission http:cookies', 'déclarée mais aucun cookie lu ni déposé');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
r.in('Sécurité');
|
|
166
|
+
const rules: [RegExp, string, string][] = [
|
|
167
|
+
[/\bprocess\.env\b/, 'process.env', 'lecture directe des variables du serveur : utilisez ctx.env() et la permission env:'],
|
|
168
|
+
[/\bprocess\.(?:exit|kill|binding|dlopen|chdir|setuid)\b/, 'process', 'contrôle du processus du site'],
|
|
169
|
+
[/(?:^|[^\w$.])require\s*\(|createRequire\s*\(/, 'require()', 'chargement de modules Node hors bundle'],
|
|
170
|
+
[/(?:^|[^\w$.])fetch\s*\(|globalThis\.fetch\b|\bXMLHttpRequest\b|\bnew\s+WebSocket\b/, 'Réseau direct', 'appel réseau qui contourne ctx.fetch() et les permissions net:'],
|
|
171
|
+
[/(?:^|[^\w$.])eval\s*\(|new\s+Function\s*\(/, 'eval / new Function', 'exécution de code construit à l’exécution'],
|
|
172
|
+
[/\bglobalThis\.__SNAPIFY|\b__dirname\b|\bimport\.meta\.url\b/, 'Accès à l’hôte', 'accès aux internes du CMS ou au système de fichiers'],
|
|
173
|
+
];
|
|
174
|
+
let clean = true;
|
|
175
|
+
for (const [re, label, detail] of rules) {
|
|
176
|
+
if (re.test(code)) {
|
|
177
|
+
clean = false;
|
|
178
|
+
r.fail(`srv-${label}`, `Serveur : ${label}`, detail);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (clean) r.pass('srv-clean', 'Code serveur', 'aucun accès direct au système, au réseau ni aux internes du CMS');
|
|
182
|
+
|
|
183
|
+
return { capabilities, envVars, hosts: hostsInCode };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function analyzeBrowser(code: string, name: string, r: Report) {
|
|
187
|
+
r.in('Sécurité');
|
|
188
|
+
const issues: string[] = [];
|
|
189
|
+
if (/(?:^|[^\w$.])eval\s*\(|new\s+Function\s*\(/.test(code)) issues.push('eval / new Function');
|
|
190
|
+
if (/\bdocument\.cookie\b/.test(code)) issues.push('lecture de document.cookie');
|
|
191
|
+
if (/localStorage\.(?:getItem|setItem)\(\s*["'`](?:snapify_admin|admin_token|px_admin)/.test(code)) issues.push('accès à la session d’administration');
|
|
192
|
+
if (/<script[\s>]|\.src\s*=\s*["'`]https?:\/\//.test(code) && /createElement\(\s*["'`]script["'`]\)/.test(code)) issues.push('injection de scripts externes');
|
|
193
|
+
if (issues.length) r.warn(`br-${name}`, `${name} : pratiques à justifier`, issues.join(', '));
|
|
194
|
+
else r.pass(`br-${name}`, `${name} : code navigateur`, 'aucune pratique à risque détectée');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Clés présentes dans un bundle minifié (les noms de propriétés ne sont pas renommés). */
|
|
198
|
+
const hasKey = (code: string, key: string) => new RegExp(`(?:["'\`]${key.replace(/[-.]/g, '\\$&')}["'\`]|[{,]\\s*${/^[A-Za-z_$][\w$]*$/.test(key) ? key : '\\0'})\\s*:`).test(code);
|
|
199
|
+
|
|
200
|
+
function analyzeShape(m: PackagedManifest, files: Map<string, Buffer>, r: Report) {
|
|
201
|
+
r.in('Code');
|
|
202
|
+
const pub = files.get('public.js')?.toString('utf8') || '';
|
|
203
|
+
const admin = files.get('admin.js')?.toString('utf8') || '';
|
|
204
|
+
if (m.kind === 'theme') {
|
|
205
|
+
const missing = ['Header', 'Footer', 'blocks'].filter((k) => !hasKey(pub, k));
|
|
206
|
+
if (missing.length) r.fail('theme-contract', 'Contrat du thème', `defineTheme sans ${missing.join(', ')} : le site ne pourrait pas s’afficher`);
|
|
207
|
+
else r.pass('theme-contract', 'Contrat du thème', 'Header, Footer et blocks fournis');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const missing: string[] = [];
|
|
211
|
+
for (const s of m.slots) if (!hasKey(pub, s)) missing.push(`emplacement « ${s} »`);
|
|
212
|
+
if (m.routes?.length && !hasKey(pub, 'page')) missing.push('composant page (routes déclarées)');
|
|
213
|
+
for (const w of m.widgets || []) if (!hasKey(pub, w.id)) missing.push(`widget « ${w.id} »`);
|
|
214
|
+
for (const p of m.adminPages || []) if (!hasKey(admin, p.id)) missing.push(`page d’administration « ${p.id} »`);
|
|
215
|
+
const declaredCount = m.slots.length + (m.routes?.length ? 1 : 0) + (m.widgets?.length || 0) + (m.adminPages?.length || 0);
|
|
216
|
+
if (missing.length) r.warn('shape', 'Composants déclarés', `introuvables dans le code : ${missing.join(', ')}`);
|
|
217
|
+
else if (declaredCount) r.pass('shape', 'Composants déclarés', `${declaredCount} élément(s) du manifeste trouvés dans le code`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function analyzeCss(m: PackagedManifest, files: Map<string, Buffer>, r: Report) {
|
|
221
|
+
r.in('Sécurité');
|
|
222
|
+
const { default: postcss } = await import('postcss');
|
|
223
|
+
for (const name of ['public.css', 'admin.css']) {
|
|
224
|
+
const css = files.get(name);
|
|
225
|
+
if (!css) continue;
|
|
226
|
+
let root;
|
|
227
|
+
try {
|
|
228
|
+
root = postcss.parse(css.toString('utf8'));
|
|
229
|
+
} catch (e) {
|
|
230
|
+
r.fail(`css-${name}`, `${name}`, `feuille de style illisible : ${(e as Error).message.slice(0, 120)}`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const remote: string[] = [];
|
|
234
|
+
root.walkAtRules('import', (a) => {
|
|
235
|
+
if (/https?:/.test(a.params)) remote.push(a.params.slice(0, 80));
|
|
236
|
+
});
|
|
237
|
+
if (m.kind === 'theme') {
|
|
238
|
+
if (remote.length) r.warn(`css-${name}`, `${name}`, `ressources externes importées : ${remote.join(', ')}`);
|
|
239
|
+
else r.pass(`css-${name}`, `${name}`, 'feuille globale du thème');
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const scope = `:where([data-snapify-scope="${m.id}"])`;
|
|
243
|
+
const leaks: string[] = [];
|
|
244
|
+
root.walkRules((rule) => {
|
|
245
|
+
const parent = rule.parent as { type?: string; name?: string } | undefined;
|
|
246
|
+
if (parent?.type === 'atrule' && /keyframes$/i.test(parent.name || '')) return;
|
|
247
|
+
for (const s of rule.selectors) {
|
|
248
|
+
const t = s.trim();
|
|
249
|
+
const rooted = /^(:root|:host|html|body)\b/.test(t) && t.includes(scope);
|
|
250
|
+
if (!t.startsWith(scope) && !rooted && !/^(:root|:host)$/.test(t)) leaks.push(t);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
if (leaks.length) r.fail(`css-${name}`, `${name} : portée des styles`, `${leaks.length} sélecteur(s) hors du conteneur du module (ex. ${leaks.slice(0, 3).join(' · ')}) : il modifierait le thème du site`);
|
|
254
|
+
else if (remote.length) r.warn(`css-${name}`, `${name}`, `ressources externes importées : ${remote.join(', ')}`);
|
|
255
|
+
else r.pass(`css-${name}`, `${name} : portée des styles`, 'toutes les règles sont limitées au conteneur du module');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* ------------------------------------------------------------------ Présentation */
|
|
260
|
+
|
|
261
|
+
function imageSize(buf: Buffer): { type: string; width: number; height: number } | null {
|
|
262
|
+
if (buf.length > 24 && buf.readUInt32BE(0) === 0x89504e47) return { type: 'png', width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
263
|
+
if (buf.length > 4 && buf[0] === 0xff && buf[1] === 0xd8) {
|
|
264
|
+
let i = 2;
|
|
265
|
+
while (i + 9 < buf.length) {
|
|
266
|
+
if (buf[i] !== 0xff) return null;
|
|
267
|
+
const marker = buf[i + 1];
|
|
268
|
+
const len = buf.readUInt16BE(i + 2);
|
|
269
|
+
if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker)) return { type: 'jpg', height: buf.readUInt16BE(i + 5), width: buf.readUInt16BE(i + 7) };
|
|
270
|
+
i += 2 + len;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
if (buf.length > 30 && buf.toString('ascii', 0, 4) === 'RIFF' && buf.toString('ascii', 8, 12) === 'WEBP') {
|
|
275
|
+
const chunk = buf.toString('ascii', 12, 16);
|
|
276
|
+
if (chunk === 'VP8 ') return { type: 'webp', width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff };
|
|
277
|
+
if (chunk === 'VP8L') {
|
|
278
|
+
const b = buf.readUInt32LE(21);
|
|
279
|
+
return { type: 'webp', width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1 };
|
|
280
|
+
}
|
|
281
|
+
if (chunk === 'VP8X') return { type: 'webp', width: buf.readUIntLE(24, 3) + 1, height: buf.readUIntLE(27, 3) + 1 };
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const SECRET_KEY = /(secret|token|password|passwd|pwd|api_?key|private_?key|access_?key|client_?secret)$/i;
|
|
287
|
+
|
|
288
|
+
function analyzePresentation(m: PackagedManifest, files: Map<string, Buffer>, r: Report) {
|
|
289
|
+
r.in('Sécurité');
|
|
290
|
+
const exposed = [...m.settings, ...(m.widgets || []).flatMap((w) => w.fields)].filter((f) => SECRET_KEY.test(f.key) && (f.public || m.kind === 'theme'));
|
|
291
|
+
if (exposed.length) r.fail('settings-secret', 'Réglages publics', `${exposed.map((f) => f.key).join(', ')} ressemble à un secret mais serait envoyé au navigateur`);
|
|
292
|
+
else r.pass('settings-secret', 'Réglages publics', `${m.settings.filter((f) => f.public).length} réglage(s) public(s), aucun secret exposé`);
|
|
293
|
+
|
|
294
|
+
r.in('Présentation');
|
|
295
|
+
const shot = m.screenshot ? files.get(m.screenshot) : undefined;
|
|
296
|
+
if (!shot) r.warn('screenshot', 'Capture d’écran', 'absente : ajoutez "screenshot" au manifeste (1280 × 720 conseillé)');
|
|
297
|
+
else {
|
|
298
|
+
const dim = imageSize(shot);
|
|
299
|
+
const ext = m.screenshot!.split('.').pop();
|
|
300
|
+
if (!dim) r.fail('screenshot', 'Capture d’écran', 'fichier image illisible');
|
|
301
|
+
else if (dim.type !== ext) r.fail('screenshot', 'Capture d’écran', `le contenu (${dim.type}) ne correspond pas à l’extension .${ext}`);
|
|
302
|
+
else if (dim.width < 800 || dim.height < 450) r.warn('screenshot', 'Capture d’écran', `${dim.width} × ${dim.height} : trop petite pour la marketplace (1280 × 720 conseillé)`);
|
|
303
|
+
else r.pass('screenshot', 'Capture d’écran', `${dim.width} × ${dim.height}, ${kb(shot.length)}`);
|
|
304
|
+
}
|
|
305
|
+
if (files.has('README.md')) r.pass('readme', 'README', kb(files.get('README.md')!.length));
|
|
306
|
+
else r.warn('readme', 'README', 'absent : décrivez l’installation et les réglages pour les clients');
|
|
307
|
+
if (m.description.trim().length < 40) r.warn('description', 'Description', 'moins de 40 caractères : elle sert de résumé sur la marketplace');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* ------------------------------------------------------------------ Analyse */
|
|
311
|
+
|
|
312
|
+
const BUNDLE_LIMITS: Record<string, number> = { 'public.js': 900 * 1024, 'admin.js': 1.5 * 1024 * 1024, 'server.mjs': 2 * 1024 * 1024 };
|
|
313
|
+
|
|
314
|
+
/** Analyse complète d’un paquet .snapmod / .snaptheme (structure, empreintes, signature, code, cohérence). */
|
|
315
|
+
export async function validateMarketplacePackage(
|
|
316
|
+
buf: Buffer,
|
|
317
|
+
opts?: { expectedKind?: 'module' | 'theme'; expectedModuleId?: string; expectedVersion?: string }
|
|
318
|
+
): Promise<PackageValidationResult> {
|
|
319
|
+
const r = new Report('Paquet');
|
|
320
|
+
const size = buf.length;
|
|
321
|
+
const digest = hashBuf(buf);
|
|
322
|
+
const result = (extra: Partial<PackageValidationResult> = {}): PackageValidationResult => ({
|
|
323
|
+
ok: r.errors.length === 0,
|
|
324
|
+
checks: r.checks,
|
|
325
|
+
signed: false,
|
|
326
|
+
hasLicense: false,
|
|
327
|
+
fileCount: 0,
|
|
328
|
+
size,
|
|
329
|
+
sha256: digest,
|
|
330
|
+
errors: r.errors,
|
|
331
|
+
warnings: r.warnings,
|
|
332
|
+
...extra,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (size <= 0 || size > 20 * 1024 * 1024) r.fail('size', 'Taille du fichier', `${kb(size)} — 20 Mo maximum`);
|
|
336
|
+
else r.pass('size', 'Taille du fichier', kb(size));
|
|
337
|
+
|
|
338
|
+
let pkg: ModulePackage;
|
|
339
|
+
let files: Map<string, Buffer>;
|
|
340
|
+
try {
|
|
341
|
+
({ pkg, files } = decodePackage(buf));
|
|
342
|
+
} catch (e) {
|
|
343
|
+
r.fail('format', 'Format et manifeste', e instanceof PackageError ? e.message : 'Paquet illisible');
|
|
344
|
+
return result();
|
|
345
|
+
}
|
|
346
|
+
const m = pkg.manifest;
|
|
347
|
+
const kind = m.kind === 'theme' ? 'theme' : 'module';
|
|
348
|
+
r.pass('format', 'Format snapmod/1', `${files.size} fichier(s) : ${[...files.keys()].join(', ')}`);
|
|
349
|
+
r.pass('integrity', 'Empreintes SHA-256', 'chaque fichier correspond au manifeste');
|
|
350
|
+
|
|
351
|
+
const signed = verify(m, pkg.signature, TRUSTED_KEYS);
|
|
352
|
+
if (signed) r.pass('signature', 'Signature NOVALABS', pkg.signature?.kid);
|
|
353
|
+
else if (pkg.signature?.kid && TRUSTED_KEYS[pkg.signature.kid]) r.fail('signature', 'Signature', 'ne correspond plus au contenu : le paquet a été modifié après sa signature');
|
|
354
|
+
else if (pkg.signature) r.warn('signature', 'Signature', 'présente mais d’une clé non reconnue : NOVALABS signera la version validée');
|
|
355
|
+
else r.warn('signature', 'Signature', 'paquet non signé : NOVALABS signera la version validée lors de la revue');
|
|
356
|
+
|
|
357
|
+
const lic = pkg.license;
|
|
358
|
+
if (lic) {
|
|
359
|
+
const issues = checkLicensePayload(lic.payload);
|
|
360
|
+
if (!verify(lic.payload, lic.signature, TRUSTED_KEYS)) r.fail('license', 'Licence embarquée', 'signature de licence invalide');
|
|
361
|
+
else if (lic.payload.moduleId !== m.id) r.fail('license', 'Licence embarquée', `délivrée pour ${lic.payload.moduleId}, pas pour ${m.id}`);
|
|
362
|
+
else if (issues.length) r.fail('license', 'Licence embarquée', issues.join(', '));
|
|
363
|
+
else if (lic.payload.expiresAt && Date.parse(lic.payload.expiresAt) < Date.now()) r.fail('license', 'Licence embarquée', 'expirée');
|
|
364
|
+
else if (m.pricing === 'paid' && lic.payload.domains.includes('*')) r.warn('license', 'Licence embarquée', 'module payant livré avec une licence valable partout : il serait utilisable sans achat');
|
|
365
|
+
else r.pass('license', 'Licence embarquée', `${lic.payload.plan}, ${lic.payload.domains.join(', ')}`);
|
|
366
|
+
} else {
|
|
367
|
+
r.pass('license', 'Licence', m.pricing === 'paid' ? 'délivrée au client à chaque achat' : 'licence gratuite ajoutée à la signature');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
r.in('Manifeste');
|
|
371
|
+
r.pass('manifest', 'Manifeste', `${m.id}@${m.version} — ${kind === 'theme' ? 'thème' : 'module'} « ${m.name} »`);
|
|
372
|
+
if (opts?.expectedKind && opts.expectedKind !== kind) r.fail('kind', 'Type', `attendu : ${opts.expectedKind === 'theme' ? 'thème' : 'module'}, reçu : ${kind === 'theme' ? 'thème' : 'module'}`);
|
|
373
|
+
if (opts?.expectedModuleId && opts.expectedModuleId !== m.id) r.fail('id', 'Identifiant', `${m.id} ≠ ${opts.expectedModuleId} : une mise à jour doit garder le même identifiant`);
|
|
374
|
+
if (opts?.expectedVersion && opts.expectedVersion !== m.version) r.warn('version', 'Version', `manifeste ${m.version} ≠ version saisie ${opts.expectedVersion} — celle du manifeste est utilisée`);
|
|
375
|
+
|
|
376
|
+
const range = m.engines.snapify;
|
|
377
|
+
if (!/^(\*|(\^|>=)?\d+\.\d+\.\d+)$/.test(range.trim())) r.fail('engines', 'Compatibilité Snapify', `plage « ${range} » non reconnue (ex. ^1.3.0)`);
|
|
378
|
+
else if (!satisfies(SNAPIFY_MODULE_API_VERSION, range)) r.fail('engines', 'Compatibilité Snapify', `requiert ${range}, l’API actuelle est ${SNAPIFY_MODULE_API_VERSION} : installation refusée par les sites`);
|
|
379
|
+
else if (range.trim() === '*') r.warn('engines', 'Compatibilité Snapify', `« * » : précisez la version minimale testée (ex. ^${SNAPIFY_MODULE_API_VERSION})`);
|
|
380
|
+
else r.pass('engines', 'Compatibilité Snapify', `${range} — compatible avec l’API ${SNAPIFY_MODULE_API_VERSION}`);
|
|
381
|
+
|
|
382
|
+
if (m.requires && Object.keys(m.requires).length) {
|
|
383
|
+
r.pass('requires', 'Dépendances', Object.entries(m.requires).map(([id, v]) => `${id} ${v}`).join(', '));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
r.in('Code');
|
|
387
|
+
const fileStats: { name: string; size: number }[] = [];
|
|
388
|
+
let srv: ReturnType<typeof analyzeServer> | undefined;
|
|
389
|
+
for (const name of ['public.js', 'admin.js', 'server.mjs'] as const) {
|
|
390
|
+
const data = files.get(name);
|
|
391
|
+
if (!data) continue;
|
|
392
|
+
fileStats.push({ name, size: data.length });
|
|
393
|
+
const code = data.toString('utf8');
|
|
394
|
+
const info = await parseBundle(code, name);
|
|
395
|
+
if ('error' in info) {
|
|
396
|
+
r.fail(`syntax-${name}`, `${name} : syntaxe`, info.error);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (!info.exports.includes('default')) {
|
|
400
|
+
r.fail(`export-${name}`, `${name} : export`, 'pas d’export par défaut (definePublicModule / defineServerModule / defineAdminModule / defineTheme)');
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const bad: string[] = [];
|
|
404
|
+
const forbidden: string[] = [];
|
|
405
|
+
for (const imp of info.imports) {
|
|
406
|
+
const spec = imp.path.replace(/^node:/, '');
|
|
407
|
+
if (imp.kind === 'dynamic-import' || imp.kind === 'require-call') forbidden.push(`${imp.kind === 'dynamic-import' ? 'import()' : 'require'} « ${imp.path} »`);
|
|
408
|
+
else if (name !== 'server.mjs') bad.push(imp.path);
|
|
409
|
+
else if (NODE_FORBIDDEN.has(spec)) forbidden.push(imp.path);
|
|
410
|
+
else if (!NODE_ALLOWED.has(spec)) bad.push(imp.path);
|
|
411
|
+
}
|
|
412
|
+
if (forbidden.length) r.fail(`imports-${name}`, `${name} : imports`, `accès interdit : ${forbidden.join(', ')}`);
|
|
413
|
+
else if (bad.length) r.fail(`imports-${name}`, `${name} : imports`, `${bad.join(', ')} non inclus dans le bundle : le module ne se chargera pas`);
|
|
414
|
+
else {
|
|
415
|
+
const node = info.imports.map((i) => i.path);
|
|
416
|
+
r.pass(`imports-${name}`, `${name}`, `syntaxe valide, export par défaut, ${node.length ? `imports autorisés : ${node.join(', ')}` : 'aucune dépendance externe'} — ${kb(data.length)}`);
|
|
417
|
+
}
|
|
418
|
+
if (data.length > BUNDLE_LIMITS[name]) r.warn(`weight-${name}`, `${name} : poids`, `${kb(data.length)} : allège le chargement des pages (limite conseillée ${kb(BUNDLE_LIMITS[name])})`);
|
|
419
|
+
|
|
420
|
+
if (name === 'server.mjs') {
|
|
421
|
+
srv = analyzeServer(code, m, r);
|
|
422
|
+
r.in('Code');
|
|
423
|
+
} else {
|
|
424
|
+
analyzeBrowser(code, name, r);
|
|
425
|
+
r.in('Code');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (!files.has('server.mjs') && m.permissions.length) {
|
|
429
|
+
r.in('Permissions').warn('perm-noserver', 'Permissions', `${m.permissions.join(', ')} déclarées sans code serveur : elles ne servent à rien`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
analyzeShape(m, files, r);
|
|
433
|
+
await analyzeCss(m, files, r);
|
|
434
|
+
analyzePresentation(m, files, r);
|
|
435
|
+
|
|
436
|
+
return result({
|
|
437
|
+
manifest: m,
|
|
438
|
+
signed,
|
|
439
|
+
signatureKid: signed ? pkg.signature?.kid : undefined,
|
|
440
|
+
hasLicense: Boolean(lic),
|
|
441
|
+
fileCount: files.size,
|
|
442
|
+
analysis: { capabilities: srv?.capabilities || [], envVars: srv?.envVars || [], hosts: srv?.hosts || [], files: fileStats.map((f) => ({ name: f.name, size: f.size })) },
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Contrôles qui dépendent de la marketplace (propriété de l’identifiant, version suivante). */
|
|
447
|
+
export function addMarketplaceChecks(
|
|
448
|
+
report: PackageValidationResult,
|
|
449
|
+
ctx: {
|
|
450
|
+
/** Produit visé par une nouvelle version. */
|
|
451
|
+
product?: { moduleId: string; latestVersion: string | null };
|
|
452
|
+
/** Identifiant déjà publié par un autre éditeur, ou par cet éditeur dans un autre produit. */
|
|
453
|
+
takenBy?: 'other' | 'self' | null;
|
|
454
|
+
reservedPrefix?: boolean;
|
|
455
|
+
}
|
|
456
|
+
): PackageValidationResult {
|
|
457
|
+
const m = report.manifest;
|
|
458
|
+
if (!m) return report;
|
|
459
|
+
const r = new Report('Marketplace');
|
|
460
|
+
if (ctx.reservedPrefix) r.fail('id-reserved', 'Identifiant', `le préfixe « ${m.id.split('.')[0]}. » est réservé aux extensions officielles : utilisez le nom de votre société (ex. monagence.${m.id.split('.').pop()})`);
|
|
461
|
+
if (ctx.takenBy === 'other') r.fail('id-taken', 'Identifiant', `${m.id} appartient déjà à un autre éditeur`);
|
|
462
|
+
if (ctx.takenBy === 'self') r.fail('id-taken', 'Identifiant', `${m.id} existe déjà dans votre espace : publiez une nouvelle version depuis la fiche du produit`);
|
|
463
|
+
if (ctx.product) {
|
|
464
|
+
if (ctx.product.moduleId !== m.id) r.fail('id-match', 'Identifiant', `${m.id} ≠ ${ctx.product.moduleId} : une mise à jour doit garder l’identifiant du produit`);
|
|
465
|
+
else r.pass('id-match', 'Identifiant', `${m.id} — même produit`);
|
|
466
|
+
const latest = ctx.product.latestVersion;
|
|
467
|
+
if (latest && compareVersions(m.version, latest) <= 0) r.fail('version-next', 'Version', `${m.version} doit être supérieure à la dernière version publiée (${latest})`);
|
|
468
|
+
else r.pass('version-next', 'Version', latest ? `${latest} → ${m.version}` : `première version ${m.version}`);
|
|
469
|
+
} else if (!ctx.reservedPrefix && !ctx.takenBy) {
|
|
470
|
+
r.pass('id-free', 'Identifiant', `${m.id} disponible`);
|
|
471
|
+
}
|
|
472
|
+
const checks = [...report.checks, ...r.checks];
|
|
473
|
+
const errors = [...report.errors, ...r.errors];
|
|
474
|
+
return { ...report, checks, errors, warnings: [...report.warnings, ...r.warnings], ok: errors.length === 0 };
|
|
475
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapify
|
|
3
|
+
* © 2026 NOVALABS WEB DESIGN — Tunisie
|
|
4
|
+
* Contact : contact@novalabs.tn · +216 24 734 647
|
|
5
|
+
* Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
import { and, eq, inArray, sql } from 'drizzle-orm';
|
|
9
|
+
import { db } from '../db/client';
|
|
10
|
+
import {
|
|
11
|
+
mpDownloads,
|
|
12
|
+
mpOrderItems,
|
|
13
|
+
mpOrders,
|
|
14
|
+
mpProducts,
|
|
15
|
+
mpPublishers,
|
|
16
|
+
mpVersions,
|
|
17
|
+
platformUsers,
|
|
18
|
+
} from '../db/schema';
|
|
19
|
+
import { HttpError } from '../http';
|
|
20
|
+
import { randomToken } from '../security/secrets';
|
|
21
|
+
import { flouciMockAllowed, generatePayment } from '../platform/flouci';
|
|
22
|
+
import { getMpSettings } from '../platform/settings';
|
|
23
|
+
|
|
24
|
+
function marketplacePublicUrl(): string {
|
|
25
|
+
const raw =
|
|
26
|
+
process.env.MARKETPLACE_PUBLIC_URL ||
|
|
27
|
+
process.env.PLATFORM_PUBLIC_URL ||
|
|
28
|
+
'http://marketplace.localhost:3000';
|
|
29
|
+
return raw.replace(/\/$/, '').replace(/\/api\/.*$/, '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function webhookUrl(): string {
|
|
33
|
+
return `${marketplacePublicUrl()}/api/platform/flouci/webhook`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function fulfillOrderPayment(orderId: string, flouciPaymentId: string) {
|
|
37
|
+
const [order] = await db.select().from(mpOrders).where(eq(mpOrders.id, orderId)).limit(1);
|
|
38
|
+
if (!order) return;
|
|
39
|
+
if (order.status === 'paid') return;
|
|
40
|
+
|
|
41
|
+
await db
|
|
42
|
+
.update(mpOrders)
|
|
43
|
+
.set({ status: 'paid', flouciPaymentId, paidAt: new Date() })
|
|
44
|
+
.where(eq(mpOrders.id, orderId));
|
|
45
|
+
|
|
46
|
+
const items = await db.select().from(mpOrderItems).where(eq(mpOrderItems.orderId, orderId));
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
await db.insert(mpDownloads).values({
|
|
49
|
+
id: crypto.randomUUID(),
|
|
50
|
+
orderItemId: item.id,
|
|
51
|
+
token: randomToken(24),
|
|
52
|
+
expiresAt: new Date(Date.now() + 365 * 86400_000),
|
|
53
|
+
});
|
|
54
|
+
await db
|
|
55
|
+
.update(mpProducts)
|
|
56
|
+
.set({ salesCount: sql`${mpProducts.salesCount} + 1`, updatedAt: new Date() })
|
|
57
|
+
.where(eq(mpProducts.id, item.productId));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function fulfillPublisherPayment(publisherId: string, flouciPaymentId: string) {
|
|
62
|
+
const [pub] = await db.select().from(mpPublishers).where(eq(mpPublishers.id, publisherId)).limit(1);
|
|
63
|
+
if (!pub) return;
|
|
64
|
+
if (pub.status === 'active') return;
|
|
65
|
+
|
|
66
|
+
await db
|
|
67
|
+
.update(mpPublishers)
|
|
68
|
+
.set({ status: 'active', paidAt: new Date(), flouciPaymentId })
|
|
69
|
+
.where(eq(mpPublishers.id, publisherId));
|
|
70
|
+
|
|
71
|
+
await db
|
|
72
|
+
.update(platformUsers)
|
|
73
|
+
.set({ role: 'publisher', updatedAt: new Date() })
|
|
74
|
+
.where(and(eq(platformUsers.id, pub.userId), inArray(platformUsers.role, ['member'])));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function startOrderCheckout(orderId: string, buyerId: string) {
|
|
78
|
+
const [order] = await db.select().from(mpOrders).where(and(eq(mpOrders.id, orderId), eq(mpOrders.buyerId, buyerId))).limit(1);
|
|
79
|
+
if (!order) throw new HttpError(404, 'Commande introuvable');
|
|
80
|
+
if (order.status === 'paid') throw new HttpError(400, 'Commande déjà payée');
|
|
81
|
+
|
|
82
|
+
const trackingId = `order_${order.id}`;
|
|
83
|
+
const base = marketplacePublicUrl();
|
|
84
|
+
const payment = await generatePayment({
|
|
85
|
+
amountMillimes: order.totalMillimes,
|
|
86
|
+
trackingId,
|
|
87
|
+
successLink: `${base}/checkout/success?order=${order.id}`,
|
|
88
|
+
failLink: `${base}/checkout/fail?order=${order.id}`,
|
|
89
|
+
webhook: webhookUrl(),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await db
|
|
93
|
+
.update(mpOrders)
|
|
94
|
+
.set({ flouciPaymentId: payment.paymentId, flouciTrackingId: trackingId })
|
|
95
|
+
.where(eq(mpOrders.id, order.id));
|
|
96
|
+
|
|
97
|
+
// Auto-complete en mock (dev uniquement)
|
|
98
|
+
if (flouciMockAllowed() || (payment.paymentId.startsWith('mock_') && process.env.NODE_ENV !== 'production')) {
|
|
99
|
+
await fulfillOrderPayment(order.id, payment.paymentId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return payment;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function startPublisherFeeCheckout(publisherId: string, userId: string) {
|
|
106
|
+
const [pub] = await db
|
|
107
|
+
.select()
|
|
108
|
+
.from(mpPublishers)
|
|
109
|
+
.where(and(eq(mpPublishers.id, publisherId), eq(mpPublishers.userId, userId)))
|
|
110
|
+
.limit(1);
|
|
111
|
+
if (!pub) throw new HttpError(404, 'Inscription développeur introuvable');
|
|
112
|
+
if (pub.status === 'active') throw new HttpError(400, 'Déjà inscrit');
|
|
113
|
+
|
|
114
|
+
const settings = await getMpSettings();
|
|
115
|
+
const trackingId = `publisher_${pub.id}`;
|
|
116
|
+
const base = marketplacePublicUrl();
|
|
117
|
+
const payment = await generatePayment({
|
|
118
|
+
amountMillimes: settings.developerFeeMillimes,
|
|
119
|
+
trackingId,
|
|
120
|
+
successLink: `${base}/developer/register/success?publisher=${pub.id}`,
|
|
121
|
+
failLink: `${base}/developer/register?fail=1`,
|
|
122
|
+
webhook: webhookUrl(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await db.update(mpPublishers).set({ flouciPaymentId: payment.paymentId }).where(eq(mpPublishers.id, pub.id));
|
|
126
|
+
|
|
127
|
+
if (flouciMockAllowed() || (payment.paymentId.startsWith('mock_') && process.env.NODE_ENV !== 'production')) {
|
|
128
|
+
await fulfillPublisherPayment(pub.id, payment.paymentId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return payment;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function createOrderFromProducts(buyerId: string, productIds: string[]) {
|
|
135
|
+
const unique = [...new Set(productIds)];
|
|
136
|
+
if (!unique.length) throw new HttpError(422, 'Panier vide');
|
|
137
|
+
|
|
138
|
+
const products = await db.select().from(mpProducts).where(and(eq(mpProducts.status, 'published'), inArray(mpProducts.id, unique)));
|
|
139
|
+
if (products.length !== unique.length) throw new HttpError(422, 'Certains produits sont indisponibles');
|
|
140
|
+
|
|
141
|
+
const versions = await db.select().from(mpVersions).where(inArray(mpVersions.productId, unique));
|
|
142
|
+
const latestByProduct = new Map<string, (typeof versions)[0]>();
|
|
143
|
+
for (const v of versions) {
|
|
144
|
+
const cur = latestByProduct.get(v.productId);
|
|
145
|
+
if (!cur || v.createdAt > cur.createdAt) latestByProduct.set(v.productId, v);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const p of products) {
|
|
149
|
+
if (!latestByProduct.has(p.id)) throw new HttpError(422, `Aucune version pour ${p.name}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const settings = await getMpSettings();
|
|
153
|
+
const totalMillimes = products.reduce((s, p) => s + p.priceMillimes, 0);
|
|
154
|
+
const commissionMillimes = Math.round((totalMillimes * settings.commissionBps) / 10_000);
|
|
155
|
+
|
|
156
|
+
// Gratuit : fulfillment immédiat
|
|
157
|
+
const orderId = crypto.randomUUID();
|
|
158
|
+
await db.insert(mpOrders).values({
|
|
159
|
+
id: orderId,
|
|
160
|
+
buyerId,
|
|
161
|
+
totalMillimes,
|
|
162
|
+
commissionMillimes,
|
|
163
|
+
status: totalMillimes === 0 ? 'paid' : 'pending',
|
|
164
|
+
paidAt: totalMillimes === 0 ? new Date() : null,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
for (const p of products) {
|
|
168
|
+
const ver = latestByProduct.get(p.id)!;
|
|
169
|
+
const itemId = crypto.randomUUID();
|
|
170
|
+
await db.insert(mpOrderItems).values({
|
|
171
|
+
id: itemId,
|
|
172
|
+
orderId,
|
|
173
|
+
productId: p.id,
|
|
174
|
+
versionId: ver.id,
|
|
175
|
+
unitPriceMillimes: p.priceMillimes,
|
|
176
|
+
licenseKey: `SNAP-${randomToken(8).toUpperCase()}`,
|
|
177
|
+
});
|
|
178
|
+
if (totalMillimes === 0) {
|
|
179
|
+
await db.insert(mpDownloads).values({
|
|
180
|
+
id: crypto.randomUUID(),
|
|
181
|
+
orderItemId: itemId,
|
|
182
|
+
token: randomToken(24),
|
|
183
|
+
expiresAt: new Date(Date.now() + 365 * 86400_000),
|
|
184
|
+
});
|
|
185
|
+
await db
|
|
186
|
+
.update(mpProducts)
|
|
187
|
+
.set({ salesCount: sql`${mpProducts.salesCount} + 1`, updatedAt: new Date() })
|
|
188
|
+
.where(eq(mpProducts.id, p.id));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return orderId;
|
|
193
|
+
}
|