create-snapify-app 0.0.0-stage → 1.1.0

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.
Files changed (614) hide show
  1. package/LICENSE.md +24 -0
  2. package/README.md +55 -2
  3. package/bin/create-snapify-app.mjs +174 -0
  4. package/lib/args.mjs +59 -0
  5. package/lib/copy.mjs +44 -0
  6. package/lib/env.mjs +84 -0
  7. package/lib/prompt.mjs +63 -0
  8. package/package.json +33 -4
  9. package/template/.env.example +79 -0
  10. package/template/.github/workflows/ci.yml +23 -0
  11. package/template/.github/workflows/deploy.yml +26 -0
  12. package/template/.github/workflows/ping.yml +8 -0
  13. package/template/.github/workflows/release.yml +67 -0
  14. package/template/.snapify-install.json +5 -0
  15. package/template/CHANGELOG.md +28 -0
  16. package/template/LICENSE +14 -0
  17. package/template/README.md +287 -0
  18. package/template/administrator.html +23 -0
  19. package/template/apps/client/src/App.tsx +765 -0
  20. package/template/apps/client/src/auth.css +400 -0
  21. package/template/apps/client/src/lib/api.ts +22 -0
  22. package/template/apps/client/src/main.tsx +20 -0
  23. package/template/apps/community/src/App.tsx +592 -0
  24. package/template/apps/community/src/main.tsx +20 -0
  25. package/template/apps/community/src/styles.css +24 -0
  26. package/template/apps/developers/src/App.tsx +643 -0
  27. package/template/apps/developers/src/main.tsx +17 -0
  28. package/template/apps/developers/src/pages.tsx +1587 -0
  29. package/template/apps/developers/src/styles.css +98 -0
  30. package/template/apps/developers/src/ui.tsx +179 -0
  31. package/template/apps/landing/src/App.tsx +598 -0
  32. package/template/apps/landing/src/LegalPages.tsx +158 -0
  33. package/template/apps/landing/src/OrderPage.tsx +381 -0
  34. package/template/apps/landing/src/SeoPages.tsx +923 -0
  35. package/template/apps/landing/src/copy.ts +195 -0
  36. package/template/apps/landing/src/main.tsx +20 -0
  37. package/template/apps/landing/src/styles.css +803 -0
  38. package/template/apps/landing/src/types.ts +19 -0
  39. package/template/apps/marketplace/src/App.tsx +1229 -0
  40. package/template/apps/marketplace/src/developer/DeveloperApp.tsx +1266 -0
  41. package/template/apps/marketplace/src/main.tsx +20 -0
  42. package/template/apps/marketplace/src/styles.css +1001 -0
  43. package/template/apps/shared/AuthContext.tsx +65 -0
  44. package/template/apps/shared/Logo.tsx +33 -0
  45. package/template/apps/shared/api.ts +47 -0
  46. package/template/apps/shared/ui.tsx +128 -0
  47. package/template/apps/superadmin/src/App.tsx +1264 -0
  48. package/template/apps/superadmin/src/lib/api.ts +20 -0
  49. package/template/apps/superadmin/src/lib/hooks.ts +20 -0
  50. package/template/apps/superadmin/src/main.tsx +16 -0
  51. package/template/apps/superadmin/src/pages/ClientsPage.tsx +185 -0
  52. package/template/apps/superadmin/src/pages/InvoicesPage.tsx +157 -0
  53. package/template/apps/superadmin/src/pages/NewsletterPage.tsx +212 -0
  54. package/template/apps/superadmin/src/pages/OrdersPage.tsx +195 -0
  55. package/template/apps/superadmin/src/pages/PaymentsPage.tsx +150 -0
  56. package/template/apps/superadmin/src/pages/PayoutsPage.tsx +133 -0
  57. package/template/apps/superadmin/src/pages/PublishersPage.tsx +151 -0
  58. package/template/apps/superadmin/src/pages/SeoControlPage.tsx +692 -0
  59. package/template/apps/superadmin/src/pages/SettingsPage.tsx +199 -0
  60. package/template/apps/superadmin/src/pages/SystemPage.tsx +109 -0
  61. package/template/apps/superadmin/src/pages/TicketsPage.tsx +201 -0
  62. package/template/apps/superadmin/src/pages/UsersPage.tsx +167 -0
  63. package/template/client.html +21 -0
  64. package/template/community/hamdiayari/akhbar/README.md +23 -0
  65. package/template/community/hamdiayari/akhbar/screenshot.jpg +0 -0
  66. package/template/community/hamdiayari/akhbar/snapify.theme.json +76 -0
  67. package/template/community/hamdiayari/akhbar/src/Brand.tsx +26 -0
  68. package/template/community/hamdiayari/akhbar/src/Footer.tsx +182 -0
  69. package/template/community/hamdiayari/akhbar/src/Header.tsx +191 -0
  70. package/template/community/hamdiayari/akhbar/src/Layout.tsx +49 -0
  71. package/template/community/hamdiayari/akhbar/src/Social.tsx +33 -0
  72. package/template/community/hamdiayari/akhbar/src/Ticker.tsx +86 -0
  73. package/template/community/hamdiayari/akhbar/src/Views.tsx +285 -0
  74. package/template/community/hamdiayari/akhbar/src/blocks/CollectionBlocks.tsx +672 -0
  75. package/template/community/hamdiayari/akhbar/src/blocks/ContentBlocks.tsx +305 -0
  76. package/template/community/hamdiayari/akhbar/src/blocks/ConversionBlocks.tsx +569 -0
  77. package/template/community/hamdiayari/akhbar/src/blocks/HeaderBlocks.tsx +287 -0
  78. package/template/community/hamdiayari/akhbar/src/index.tsx +51 -0
  79. package/template/community/hamdiayari/akhbar/src/kit.tsx +357 -0
  80. package/template/community/hamdiayari/akhbar/src/site.ts +96 -0
  81. package/template/community/hamdiayari/akhbar/src/styles.css +566 -0
  82. package/template/community/hamdiayari/akhbar-actualites/README.md +28 -0
  83. package/template/community/hamdiayari/akhbar-actualites/screenshot.jpg +0 -0
  84. package/template/community/hamdiayari/akhbar-actualites/snapify.module.json +107 -0
  85. package/template/community/hamdiayari/akhbar-actualites/src/admin/AudiencePage.tsx +185 -0
  86. package/template/community/hamdiayari/akhbar-actualites/src/admin/index.tsx +11 -0
  87. package/template/community/hamdiayari/akhbar-actualites/src/public/Archive.tsx +142 -0
  88. package/template/community/hamdiayari/akhbar-actualites/src/public/cards.tsx +152 -0
  89. package/template/community/hamdiayari/akhbar-actualites/src/public/index.tsx +71 -0
  90. package/template/community/hamdiayari/akhbar-actualites/src/public/posts.ts +103 -0
  91. package/template/community/hamdiayari/akhbar-actualites/src/public/styles.css +568 -0
  92. package/template/community/hamdiayari/akhbar-actualites/src/public/widgets.tsx +226 -0
  93. package/template/community/hamdiayari/akhbar-actualites/src/server.ts +119 -0
  94. package/template/community/hamdiayari/akhbar-actualites/src/types.ts +25 -0
  95. package/template/community/hamdiayari/akhbar-urgent/README.md +36 -0
  96. package/template/community/hamdiayari/akhbar-urgent/screenshot.jpg +0 -0
  97. package/template/community/hamdiayari/akhbar-urgent/snapify.module.json +64 -0
  98. package/template/community/hamdiayari/akhbar-urgent/src/admin/FlashesPage.tsx +229 -0
  99. package/template/community/hamdiayari/akhbar-urgent/src/admin/index.tsx +11 -0
  100. package/template/community/hamdiayari/akhbar-urgent/src/public/Ticker.tsx +81 -0
  101. package/template/community/hamdiayari/akhbar-urgent/src/public/index.tsx +63 -0
  102. package/template/community/hamdiayari/akhbar-urgent/src/public/styles.css +142 -0
  103. package/template/community/hamdiayari/akhbar-urgent/src/server.ts +101 -0
  104. package/template/community/hamdiayari/akhbar-urgent/src/types.ts +35 -0
  105. package/template/community.html +21 -0
  106. package/template/developers.html +24 -0
  107. package/template/docker-compose.db.yml +24 -0
  108. package/template/docs/BACKOFFICE_SNAPIFY.md +130 -0
  109. package/template/docs/CI_CD.md +66 -0
  110. package/template/docs/MODULES.md +346 -0
  111. package/template/docs/PLATFORM_DEPLOY.md +54 -0
  112. package/template/docs/logo.svg +10 -0
  113. package/template/drizzle/0000_init.sql +147 -0
  114. package/template/drizzle/0001_page_project.sql +5 -0
  115. package/template/drizzle/0002_modules.sql +23 -0
  116. package/template/drizzle/0003_themes.sql +4 -0
  117. package/template/drizzle/0004_security.sql +82 -0
  118. package/template/drizzle/0005_module_records.sql +16 -0
  119. package/template/drizzle/0006_avatars.sql +4 -0
  120. package/template/drizzle/0007_platform.sql +264 -0
  121. package/template/drizzle/0008_mp_validation.sql +3 -0
  122. package/template/drizzle/0009_saas_ops.sql +77 -0
  123. package/template/drizzle/0010_cms_chrome.sql +13 -0
  124. package/template/drizzle/0011_client_portal.sql +26 -0
  125. package/template/drizzle/meta/0000_snapshot.json +998 -0
  126. package/template/drizzle/meta/0001_snapshot.json +1004 -0
  127. package/template/drizzle/meta/_journal.json +90 -0
  128. package/template/drizzle.config.ts +16 -0
  129. package/template/ecosystem.snapify.cjs +24 -0
  130. package/template/for.html +1 -0
  131. package/template/index.html +29 -0
  132. package/template/landing.html +37 -0
  133. package/template/marketplace/catalog.json +373 -0
  134. package/template/marketplace/gallery/hamdiayari.preveris/01-home.jpg +0 -0
  135. package/template/marketplace/gallery/hamdiayari.recaptcha/01-reglages.jpg +0 -0
  136. package/template/marketplace/gallery/hamdiayari.recaptcha/02-logo.jpg +0 -0
  137. package/template/marketplace/gallery/novalabs.ai-articles/01-generer.jpg +0 -0
  138. package/template/marketplace/gallery/novalabs.ai-articles/02-fournisseur-ia.jpg +0 -0
  139. package/template/marketplace/gallery/novalabs.calendar/01-agenda-site.jpg +0 -0
  140. package/template/marketplace/gallery/novalabs.calendar/02-evenements-admin.jpg +0 -0
  141. package/template/marketplace/gallery/novalabs.calendar/03-reglages.jpg +0 -0
  142. package/template/marketplace/screenshots/hamdiayari.preveris-1.0.6.jpg +0 -0
  143. package/template/marketplace/screenshots/hamdiayari.recaptcha-1.0.0.jpg +0 -0
  144. package/template/marketplace/screenshots/hamdiayari.wp-seo-migrator-1.1.1.jpg +0 -0
  145. package/template/marketplace/screenshots/novalabs.ai-articles-1.0.1.jpg +0 -0
  146. package/template/marketplace/screenshots/novalabs.ai-chat-1.0.1.jpg +0 -0
  147. package/template/marketplace/screenshots/novalabs.calendar-1.0.1.jpg +0 -0
  148. package/template/marketplace/screenshots/novalabs.cart-1.0.0.jpg +0 -0
  149. package/template/marketplace/screenshots/novalabs.checkout-1.0.0.jpg +0 -0
  150. package/template/marketplace/screenshots/novalabs.customers-1.1.0.jpg +0 -0
  151. package/template/marketplace/screenshots/novalabs.dogga-1.0.0.jpg +0 -0
  152. package/template/marketplace/screenshots/novalabs.mahdia-1.0.0.jpg +0 -0
  153. package/template/marketplace/screenshots/novalabs.shop-1.0.0.jpg +0 -0
  154. package/template/marketplace/screenshots/novalabs.sidibou-1.0.0.jpg +0 -0
  155. package/template/marketplace/screenshots/novalabs.whatsapp-1.0.1.jpg +0 -0
  156. package/template/marketplace.html +21 -0
  157. package/template/metadata.json +7 -0
  158. package/template/modules/ai-articles/screenshot.jpg +0 -0
  159. package/template/modules/ai-articles/snapify.module.json +74 -0
  160. package/template/modules/ai-articles/src/admin/ArticlesPage.tsx +162 -0
  161. package/template/modules/ai-articles/src/admin/GeneratePage.tsx +118 -0
  162. package/template/modules/ai-articles/src/admin/index.tsx +12 -0
  163. package/template/modules/ai-articles/src/public/index.tsx +119 -0
  164. package/template/modules/ai-articles/src/server.ts +287 -0
  165. package/template/modules/ai-articles/src/types.ts +30 -0
  166. package/template/modules/ai-chat/screenshot.jpg +0 -0
  167. package/template/modules/ai-chat/snapify.module.json +217 -0
  168. package/template/modules/ai-chat/src/public.tsx +198 -0
  169. package/template/modules/ai-chat/src/server.ts +142 -0
  170. package/template/modules/ai-theme-studio/README.md +15 -0
  171. package/template/modules/ai-theme-studio/screenshot.jpg +0 -0
  172. package/template/modules/ai-theme-studio/snapify.module.json +175 -0
  173. package/template/modules/ai-theme-studio/src/admin/HistoryPage.tsx +91 -0
  174. package/template/modules/ai-theme-studio/src/admin/StudioPage.tsx +414 -0
  175. package/template/modules/ai-theme-studio/src/admin/index.tsx +11 -0
  176. package/template/modules/ai-theme-studio/src/lib/images.ts +240 -0
  177. package/template/modules/ai-theme-studio/src/lib/llm.ts +123 -0
  178. package/template/modules/ai-theme-studio/src/lib/normalize.ts +714 -0
  179. package/template/modules/ai-theme-studio/src/lib/prompts.ts +198 -0
  180. package/template/modules/ai-theme-studio/src/server.ts +137 -0
  181. package/template/modules/ai-theme-studio/src/types.ts +112 -0
  182. package/template/modules/calendar/screenshot.jpg +0 -0
  183. package/template/modules/calendar/snapify.module.json +45 -0
  184. package/template/modules/calendar/src/admin/EventsPage.tsx +217 -0
  185. package/template/modules/calendar/src/admin/index.tsx +11 -0
  186. package/template/modules/calendar/src/public/index.tsx +231 -0
  187. package/template/modules/calendar/src/server.ts +143 -0
  188. package/template/modules/calendar/src/types.ts +25 -0
  189. package/template/modules/cart/screenshot.jpg +0 -0
  190. package/template/modules/cart/snapify.module.json +90 -0
  191. package/template/modules/cart/src/admin/index.tsx +265 -0
  192. package/template/modules/cart/src/public/CartPage.tsx +198 -0
  193. package/template/modules/cart/src/public/index.tsx +261 -0
  194. package/template/modules/cart/src/public/useQuote.ts +67 -0
  195. package/template/modules/cart/src/server.ts +148 -0
  196. package/template/modules/cart/src/types.ts +31 -0
  197. package/template/modules/checkout/screenshot.jpg +0 -0
  198. package/template/modules/checkout/snapify.module.json +264 -0
  199. package/template/modules/checkout/src/admin/OrderDetailPage.tsx +304 -0
  200. package/template/modules/checkout/src/admin/badges.tsx +28 -0
  201. package/template/modules/checkout/src/admin/index.tsx +236 -0
  202. package/template/modules/checkout/src/public/CheckoutPage.tsx +461 -0
  203. package/template/modules/checkout/src/public/OrderPages.tsx +153 -0
  204. package/template/modules/checkout/src/public/index.tsx +23 -0
  205. package/template/modules/checkout/src/server.ts +383 -0
  206. package/template/modules/checkout/src/types.ts +70 -0
  207. package/template/modules/customers/screenshot.jpg +0 -0
  208. package/template/modules/customers/snapify.module.json +115 -0
  209. package/template/modules/customers/src/admin/CustomerDetailPage.tsx +274 -0
  210. package/template/modules/customers/src/admin/index.tsx +194 -0
  211. package/template/modules/customers/src/public/AccountPages.tsx +470 -0
  212. package/template/modules/customers/src/public/AddressForm.tsx +115 -0
  213. package/template/modules/customers/src/public/AuthPages.tsx +349 -0
  214. package/template/modules/customers/src/public/Avatar.tsx +29 -0
  215. package/template/modules/customers/src/public/ProfileSection.tsx +291 -0
  216. package/template/modules/customers/src/public/index.tsx +137 -0
  217. package/template/modules/customers/src/public/session.ts +51 -0
  218. package/template/modules/customers/src/server.ts +421 -0
  219. package/template/modules/customers/src/types.ts +53 -0
  220. package/template/modules/recaptcha/logo.jpg +0 -0
  221. package/template/modules/recaptcha/screenshot.jpg +0 -0
  222. package/template/modules/recaptcha/snapify.module.json +99 -0
  223. package/template/modules/recaptcha/src/public/index.tsx +94 -0
  224. package/template/modules/recaptcha/src/server.ts +65 -0
  225. package/template/modules/recaptcha/src/types.ts +18 -0
  226. package/template/modules/shop/screenshot.jpg +0 -0
  227. package/template/modules/shop/snapify.module.json +340 -0
  228. package/template/modules/shop/src/admin/CategoriesPage.tsx +224 -0
  229. package/template/modules/shop/src/admin/ProductEditor.tsx +313 -0
  230. package/template/modules/shop/src/admin/ProductsPage.tsx +307 -0
  231. package/template/modules/shop/src/admin/index.tsx +14 -0
  232. package/template/modules/shop/src/public/Catalog.tsx +244 -0
  233. package/template/modules/shop/src/public/NotFound.tsx +22 -0
  234. package/template/modules/shop/src/public/ProductPage.tsx +195 -0
  235. package/template/modules/shop/src/public/index.tsx +134 -0
  236. package/template/modules/shop/src/public/useFetch.ts +26 -0
  237. package/template/modules/shop/src/server.ts +493 -0
  238. package/template/modules/shop/src/shared.ts +41 -0
  239. package/template/modules/shop/src/types.ts +60 -0
  240. package/template/modules/whatsapp/screenshot.jpg +0 -0
  241. package/template/modules/whatsapp/snapify.module.json +121 -0
  242. package/template/modules/whatsapp/src/public.tsx +119 -0
  243. package/template/modules/wp-seo-migrator/ARCHITECTURE.md +113 -0
  244. package/template/modules/wp-seo-migrator/README.md +21 -0
  245. package/template/modules/wp-seo-migrator/fixtures/aioseo-sample.csv +3 -0
  246. package/template/modules/wp-seo-migrator/fixtures/sample-exporter.json +39 -0
  247. package/template/modules/wp-seo-migrator/fixtures/sample-page.html +12 -0
  248. package/template/modules/wp-seo-migrator/fixtures/sample.htaccess +3 -0
  249. package/template/modules/wp-seo-migrator/screenshot.jpg +0 -0
  250. package/template/modules/wp-seo-migrator/snapify.module.json +87 -0
  251. package/template/modules/wp-seo-migrator/src/admin/HistoryPage.tsx +64 -0
  252. package/template/modules/wp-seo-migrator/src/admin/WizardPage.tsx +788 -0
  253. package/template/modules/wp-seo-migrator/src/admin/i18n.ts +156 -0
  254. package/template/modules/wp-seo-migrator/src/admin/index.tsx +11 -0
  255. package/template/modules/wp-seo-migrator/src/connectors/CUSTOM_CONNECTOR.md +31 -0
  256. package/template/modules/wp-seo-migrator/src/connectors/csvSeoFactory.ts +106 -0
  257. package/template/modules/wp-seo-migrator/src/connectors/exporter.ts +96 -0
  258. package/template/modules/wp-seo-migrator/src/connectors/gscCsv.ts +53 -0
  259. package/template/modules/wp-seo-migrator/src/connectors/htaccess.ts +65 -0
  260. package/template/modules/wp-seo-migrator/src/connectors/htmlCrawler.ts +90 -0
  261. package/template/modules/wp-seo-migrator/src/connectors/index.ts +113 -0
  262. package/template/modules/wp-seo-migrator/src/connectors/pluginCsv.ts +77 -0
  263. package/template/modules/wp-seo-migrator/src/connectors/rankMathCsv.ts +77 -0
  264. package/template/modules/wp-seo-migrator/src/connectors/redirection.ts +62 -0
  265. package/template/modules/wp-seo-migrator/src/connectors/sitemap.ts +44 -0
  266. package/template/modules/wp-seo-migrator/src/connectors/utils.ts +111 -0
  267. package/template/modules/wp-seo-migrator/src/connectors/wxr.ts +103 -0
  268. package/template/modules/wp-seo-migrator/src/connectors/yoastCsv.ts +56 -0
  269. package/template/modules/wp-seo-migrator/src/pipeline/analyze.ts +158 -0
  270. package/template/modules/wp-seo-migrator/src/pipeline/htmlParse.ts +58 -0
  271. package/template/modules/wp-seo-migrator/src/pipeline/remoteAuto.ts +328 -0
  272. package/template/modules/wp-seo-migrator/src/server.ts +492 -0
  273. package/template/modules/wp-seo-migrator/src/types.ts +151 -0
  274. package/template/modules/wp-seo-migrator/tests/connectors-extra.test.ts +39 -0
  275. package/template/modules/wp-seo-migrator/tests/merge.test.ts +25 -0
  276. package/template/package-lock.json +6016 -0
  277. package/template/package.json +74 -0
  278. package/template/public/images/photos/caravane-chameaux-desert.webp +0 -0
  279. package/template/public/images/photos/hammamet-medina-plage.jpg +0 -0
  280. package/template/public/images/photos/maison-campagne-collines.webp +0 -0
  281. package/template/public/images/photos/maisons-troglodytes-desert.jpg +0 -0
  282. package/template/public/images/photos/panorama-ville.jpg +0 -0
  283. package/template/public/images/photos/preveris-accessibilite-erp.jpg +0 -0
  284. package/template/public/images/photos/preveris-collectivites.jpg +0 -0
  285. package/template/public/images/photos/preveris-etudes-plans.jpg +0 -0
  286. package/template/public/images/photos/preveris-hero-erp.jpg +0 -0
  287. package/template/public/images/photos/preveris-ssi-incendie.jpg +0 -0
  288. package/template/public/images/photos/preveris-suivi-hotel.jpg +0 -0
  289. package/template/public/images/photos/ruelle-bleue-medina.jpg +0 -0
  290. package/template/public/images/photos/sidi-bou-said-port.webp +0 -0
  291. package/template/public/images/photos/terrasse-blanche-mer-voilier.jpg +0 -0
  292. package/template/public/landing/hero-ai-cms.jpg +0 -0
  293. package/template/public/landing/shot-dogga.jpg +0 -0
  294. package/template/public/landing/shot-mahdia.jpg +0 -0
  295. package/template/public/landing/shot-sidibou.jpg +0 -0
  296. package/template/public/llms.txt +35 -0
  297. package/template/public/logo.svg +10 -0
  298. package/template/scripts/app-pack.ts +155 -0
  299. package/template/scripts/app-publish.ts +100 -0
  300. package/template/scripts/apply-ecommerce-home.ts +196 -0
  301. package/template/scripts/auto-deploy-poll.sh +13 -0
  302. package/template/scripts/db-backup.sh +16 -0
  303. package/template/scripts/db-restore.sh +11 -0
  304. package/template/scripts/deploy.sh +34 -0
  305. package/template/scripts/github-webhook-deploy.sh +7 -0
  306. package/template/scripts/install-new-modules.ts +96 -0
  307. package/template/scripts/refresh-demo-baseline.ts +9 -0
  308. package/template/scripts/reinstall-marketplace-packs.ts +51 -0
  309. package/template/scripts/restore-classic-home.ts +13 -0
  310. package/template/scripts/sdk-pack.ts +289 -0
  311. package/template/scripts/sdk-publish.ts +75 -0
  312. package/template/scripts/seed-demo-products.ts +120 -0
  313. package/template/scripts/seed-theme-content-packs.ts +42 -0
  314. package/template/scripts/snapify-module.ts +207 -0
  315. package/template/scripts/test-theme-switch.ts +24 -0
  316. package/template/sdk/admin.css +18 -0
  317. package/template/sdk/admin.ts +95 -0
  318. package/template/sdk/build.ts +249 -0
  319. package/template/sdk/commerce.ts +512 -0
  320. package/template/sdk/content.ts +76 -0
  321. package/template/sdk/icons.ts +101 -0
  322. package/template/sdk/image.ts +60 -0
  323. package/template/sdk/index.ts +500 -0
  324. package/template/sdk/package.ts +298 -0
  325. package/template/sdk/recaptcha.tsx +122 -0
  326. package/template/sdk/settings.ts +84 -0
  327. package/template/sdk/theme.css +29 -0
  328. package/template/sdk/theme.ts +100 -0
  329. package/template/sdk/ui/commerce.tsx +385 -0
  330. package/template/sdk/ui/orders.tsx +136 -0
  331. package/template/sdk-kit/CHANGELOG.md +17 -0
  332. package/template/sdk-kit/LICENSE.md +47 -0
  333. package/template/sdk-kit/README.md +81 -0
  334. package/template/sdk-kit/cli.ts +339 -0
  335. package/template/sdk-kit/create-snapify/README.md +28 -0
  336. package/template/sdk-kit/create-snapify/create-snapify.mjs +11 -0
  337. package/template/sdk-kit/create-snapify/package.template.json +16 -0
  338. package/template/sdk-kit/package.template.json +95 -0
  339. package/template/sdk-kit/templates/common/README.md.tpl +27 -0
  340. package/template/sdk-kit/templates/common/_gitignore +3 -0
  341. package/template/sdk-kit/templates/common/package.json.tpl +19 -0
  342. package/template/sdk-kit/templates/common/src/snapify-env.d.ts +2 -0
  343. package/template/sdk-kit/templates/common/tsconfig.json +15 -0
  344. package/template/sdk-kit/templates/module/snapify.module.json.tpl +33 -0
  345. package/template/sdk-kit/templates/module/src/admin.tsx +53 -0
  346. package/template/sdk-kit/templates/module/src/public.tsx +49 -0
  347. package/template/sdk-kit/templates/module/src/server.ts +30 -0
  348. package/template/sdk-kit/templates/module/src/types.ts +19 -0
  349. package/template/sdk-kit/templates/theme/snapify.theme.json.tpl +22 -0
  350. package/template/sdk-kit/templates/theme/src/Footer.tsx +40 -0
  351. package/template/sdk-kit/templates/theme/src/Header.tsx +47 -0
  352. package/template/sdk-kit/templates/theme/src/Views.tsx +36 -0
  353. package/template/sdk-kit/templates/theme/src/blocks.tsx +93 -0
  354. package/template/sdk-kit/templates/theme/src/index.tsx +30 -0
  355. package/template/sdk-kit/templates/theme/src/site.tsx +59 -0
  356. package/template/sdk-kit/templates/theme/src/styles.css +71 -0
  357. package/template/server/client/routes.ts +252 -0
  358. package/template/server/cms/analyticsStore.ts +180 -0
  359. package/template/server/cms/auth.ts +155 -0
  360. package/template/server/cms/blockCatalog.ts +342 -0
  361. package/template/server/cms/blocks.ts +521 -0
  362. package/template/server/cms/contentMigration.ts +1119 -0
  363. package/template/server/cms/defaults.ts +303 -0
  364. package/template/server/cms/permissions.ts +154 -0
  365. package/template/server/cms/restoreClassicHome.ts +183 -0
  366. package/template/server/cms/revisions.ts +99 -0
  367. package/template/server/cms/scheduler.ts +78 -0
  368. package/template/server/cms/seed.ts +72 -0
  369. package/template/server/cms/seoAnalyzer.ts +213 -0
  370. package/template/server/cms/store.ts +122 -0
  371. package/template/server/cms/themeContent.ts +1386 -0
  372. package/template/server/cms/themePacks/sidibou-branding.json +1 -0
  373. package/template/server/cms/themePacks/sidibou-home.json +1 -0
  374. package/template/server/cms/themePacks/sidibou-menus.json +1 -0
  375. package/template/server/cms/types.ts +432 -0
  376. package/template/server/community/routes.ts +410 -0
  377. package/template/server/db/client.ts +43 -0
  378. package/template/server/db/repository.ts +389 -0
  379. package/template/server/db/schema.ts +726 -0
  380. package/template/server/demo/cmsReset.ts +196 -0
  381. package/template/server/developers/sdk.ts +85 -0
  382. package/template/server/geoip.ts +15 -0
  383. package/template/server/http.ts +53 -0
  384. package/template/server/mail.ts +216 -0
  385. package/template/server/marketplace/catalog.ts +74 -0
  386. package/template/server/marketplace/packageValidate.ts +475 -0
  387. package/template/server/marketplace/payments.ts +193 -0
  388. package/template/server/marketplace/routes.ts +850 -0
  389. package/template/server/marketplace/signing.ts +116 -0
  390. package/template/server/marketplace/syncOfficial.ts +282 -0
  391. package/template/server/media/images.ts +63 -0
  392. package/template/server/middleware/adminAuth.ts +134 -0
  393. package/template/server/modules/legacy.ts +44 -0
  394. package/template/server/modules/manager.ts +1122 -0
  395. package/template/server/modules/marketplace.ts +178 -0
  396. package/template/server/modules/outbound.ts +178 -0
  397. package/template/server/modules/purchases.ts +149 -0
  398. package/template/server/modules/records.ts +168 -0
  399. package/template/server/modules/themeHtml.ts +32 -0
  400. package/template/server/modules/trust.ts +16 -0
  401. package/template/server/modules/types.ts +147 -0
  402. package/template/server/platform/auth.ts +223 -0
  403. package/template/server/platform/clientWelcome.ts +168 -0
  404. package/template/server/platform/clients.ts +139 -0
  405. package/template/server/platform/dailyImprove.ts +356 -0
  406. package/template/server/platform/flouci.ts +126 -0
  407. package/template/server/platform/host.ts +69 -0
  408. package/template/server/platform/hostingProviders.ts +143 -0
  409. package/template/server/platform/licenseOrders.ts +291 -0
  410. package/template/server/platform/money.ts +42 -0
  411. package/template/server/platform/passwords.ts +41 -0
  412. package/template/server/platform/region.ts +86 -0
  413. package/template/server/platform/routes.ts +225 -0
  414. package/template/server/platform/seed.ts +82 -0
  415. package/template/server/platform/settings.ts +72 -0
  416. package/template/server/routes/adminAccount.ts +403 -0
  417. package/template/server/routes/adminCms.ts +808 -0
  418. package/template/server/routes/adminSystem.ts +243 -0
  419. package/template/server/routes/adminUsers.ts +449 -0
  420. package/template/server/routes/googleOAuth.ts +327 -0
  421. package/template/server/routes/modules.ts +323 -0
  422. package/template/server/routes/publicCms.ts +313 -0
  423. package/template/server/routes/seoPublic.ts +158 -0
  424. package/template/server/security/accounts.ts +121 -0
  425. package/template/server/security/audit.ts +143 -0
  426. package/template/server/security/authTokens.ts +74 -0
  427. package/template/server/security/ip.ts +68 -0
  428. package/template/server/security/rateLimit.ts +85 -0
  429. package/template/server/security/retention.ts +46 -0
  430. package/template/server/security/secrets.ts +44 -0
  431. package/template/server/security/sessions.ts +140 -0
  432. package/template/server/security/totp.ts +101 -0
  433. package/template/server/superadmin/analytics.ts +109 -0
  434. package/template/server/superadmin/auth.ts +92 -0
  435. package/template/server/superadmin/backlinksCatalog.ts +210 -0
  436. package/template/server/superadmin/frSeoContent.ts +165 -0
  437. package/template/server/superadmin/landingSeo.ts +675 -0
  438. package/template/server/superadmin/ops.ts +238 -0
  439. package/template/server/superadmin/routes.ts +1095 -0
  440. package/template/server/superadmin/tnSeoContent.ts +130 -0
  441. package/template/server.ts +522 -0
  442. package/template/src/App.tsx +263 -0
  443. package/template/src/admin/AdminApp.tsx +162 -0
  444. package/template/src/admin/account/AuthLayout.tsx +57 -0
  445. package/template/src/admin/account/SecurityForms.tsx +276 -0
  446. package/template/src/admin/admin.css +127 -0
  447. package/template/src/admin/context/AuthContext.tsx +147 -0
  448. package/template/src/admin/context/SiteContext.tsx +50 -0
  449. package/template/src/admin/editor/BlockLibrary.tsx +173 -0
  450. package/template/src/admin/editor/BlocksPanel.tsx +396 -0
  451. package/template/src/admin/editor/EditorBits.tsx +106 -0
  452. package/template/src/admin/editor/PreviewFrame.tsx +145 -0
  453. package/template/src/admin/editor/RichTextEditor.tsx +276 -0
  454. package/template/src/admin/editor/SeoPanel.tsx +349 -0
  455. package/template/src/admin/editor/StyleEditor.tsx +114 -0
  456. package/template/src/admin/editor/blockSchemas.ts +699 -0
  457. package/template/src/admin/editor/fields.tsx +486 -0
  458. package/template/src/admin/layout/AdminShell.tsx +418 -0
  459. package/template/src/admin/layout/nav.ts +95 -0
  460. package/template/src/admin/lib/api.ts +408 -0
  461. package/template/src/admin/lib/audit.ts +141 -0
  462. package/template/src/admin/lib/format.ts +84 -0
  463. package/template/src/admin/lib/hooks.ts +125 -0
  464. package/template/src/admin/main.tsx +16 -0
  465. package/template/src/admin/modules/InstallDialog.tsx +160 -0
  466. package/template/src/admin/modules/SettingsForm.tsx +145 -0
  467. package/template/src/admin/modules/kit.tsx +127 -0
  468. package/template/src/admin/modules/labels.tsx +226 -0
  469. package/template/src/admin/pages/AccountSetupPage.tsx +62 -0
  470. package/template/src/admin/pages/AnalyticsPage.tsx +206 -0
  471. package/template/src/admin/pages/BrandingPage.tsx +154 -0
  472. package/template/src/admin/pages/DashboardPage.tsx +239 -0
  473. package/template/src/admin/pages/EngagementPage.tsx +168 -0
  474. package/template/src/admin/pages/FormsPage.tsx +435 -0
  475. package/template/src/admin/pages/GeneralPage.tsx +120 -0
  476. package/template/src/admin/pages/LoginPage.tsx +249 -0
  477. package/template/src/admin/pages/MaintenancePage.tsx +125 -0
  478. package/template/src/admin/pages/MarketplacePage.tsx +426 -0
  479. package/template/src/admin/pages/MediaPage.tsx +251 -0
  480. package/template/src/admin/pages/MenusPage.tsx +446 -0
  481. package/template/src/admin/pages/ModuleAdminPage.tsx +113 -0
  482. package/template/src/admin/pages/ModuleDetailPage.tsx +293 -0
  483. package/template/src/admin/pages/ModulesPage.tsx +135 -0
  484. package/template/src/admin/pages/NewsletterPage.tsx +193 -0
  485. package/template/src/admin/pages/PageEditorPage.tsx +322 -0
  486. package/template/src/admin/pages/PagesListPage.tsx +277 -0
  487. package/template/src/admin/pages/PostEditorPage.tsx +292 -0
  488. package/template/src/admin/pages/PostsListPage.tsx +187 -0
  489. package/template/src/admin/pages/ProfilePage.tsx +489 -0
  490. package/template/src/admin/pages/ResetPasswordPage.tsx +110 -0
  491. package/template/src/admin/pages/SeoPage.tsx +670 -0
  492. package/template/src/admin/pages/SettingsPage.tsx +189 -0
  493. package/template/src/admin/pages/ThemeCustomizePage.tsx +170 -0
  494. package/template/src/admin/pages/ThemesPage.tsx +292 -0
  495. package/template/src/admin/pages/UsersPage.tsx +57 -0
  496. package/template/src/admin/pages/WidgetsPage.tsx +200 -0
  497. package/template/src/admin/seo/GoogleSetup.tsx +529 -0
  498. package/template/src/admin/settings/EmailTab.tsx +293 -0
  499. package/template/src/admin/settings/OtherTabs.tsx +270 -0
  500. package/template/src/admin/settings/SecurityTab.tsx +172 -0
  501. package/template/src/admin/settings/controls.tsx +133 -0
  502. package/template/src/admin/ui/Pagination.tsx +170 -0
  503. package/template/src/admin/ui/SaveBar.tsx +77 -0
  504. package/template/src/admin/ui/TagInput.tsx +54 -0
  505. package/template/src/admin/ui/charts.tsx +108 -0
  506. package/template/src/admin/ui/media.tsx +265 -0
  507. package/template/src/admin/ui/overlays.tsx +239 -0
  508. package/template/src/admin/ui/primitives.tsx +544 -0
  509. package/template/src/admin/users/AuditTab.tsx +104 -0
  510. package/template/src/admin/users/RolesTab.tsx +368 -0
  511. package/template/src/admin/users/UsersTab.tsx +723 -0
  512. package/template/src/admin/users/shared.tsx +278 -0
  513. package/template/src/cms/icons.ts +7 -0
  514. package/template/src/components/CookieBanner.tsx +166 -0
  515. package/template/src/components/MaintenanceGate.tsx +66 -0
  516. package/template/src/components/SiteAnalytics.tsx +64 -0
  517. package/template/src/components/SnapifyLogo.tsx +44 -0
  518. package/template/src/components/cms/BlockRenderer.tsx +154 -0
  519. package/template/src/components/cms/WidgetArea.tsx +193 -0
  520. package/template/src/context/SiteConfigContext.tsx +130 -0
  521. package/template/src/demoBase.ts +23 -0
  522. package/template/src/index.css +90 -0
  523. package/template/src/main.tsx +16 -0
  524. package/template/src/modules/ModuleHost.tsx +197 -0
  525. package/template/src/preview.ts +63 -0
  526. package/template/src/runtime.ts +38 -0
  527. package/template/src/themes/ThemeHost.tsx +168 -0
  528. package/template/src/themes/fallback.tsx +71 -0
  529. package/template/src/types.ts +94 -0
  530. package/template/src/views/BlogPostView.tsx +68 -0
  531. package/template/src/views/CmsPageView.tsx +34 -0
  532. package/template/start-pm2.sh +3 -0
  533. package/template/superadmin.html +20 -0
  534. package/template/themes/ai-canvas/screenshot.jpg +0 -0
  535. package/template/themes/ai-canvas/snapify.theme.json +96 -0
  536. package/template/themes/ai-canvas/src/CeeSimulator.tsx +247 -0
  537. package/template/themes/ai-canvas/src/Footer.tsx +190 -0
  538. package/template/themes/ai-canvas/src/Header.tsx +324 -0
  539. package/template/themes/ai-canvas/src/Layout.tsx +56 -0
  540. package/template/themes/ai-canvas/src/NotFound.tsx +58 -0
  541. package/template/themes/ai-canvas/src/Post.tsx +26 -0
  542. package/template/themes/ai-canvas/src/SnapifyLogo.tsx +44 -0
  543. package/template/themes/ai-canvas/src/blocks/CollectionBlocks.tsx +711 -0
  544. package/template/themes/ai-canvas/src/blocks/ContentBlocks.tsx +318 -0
  545. package/template/themes/ai-canvas/src/blocks/ConversionBlocks.tsx +559 -0
  546. package/template/themes/ai-canvas/src/blocks/HeaderBlocks.tsx +341 -0
  547. package/template/themes/ai-canvas/src/index.tsx +54 -0
  548. package/template/themes/ai-canvas/src/kit.tsx +398 -0
  549. package/template/themes/ai-canvas/src/site.ts +29 -0
  550. package/template/themes/ai-canvas/src/styles.css +317 -0
  551. package/template/themes/dogga/screenshot.jpg +0 -0
  552. package/template/themes/dogga/snapify.theme.json +55 -0
  553. package/template/themes/dogga/src/Brand.tsx +41 -0
  554. package/template/themes/dogga/src/Footer.tsx +202 -0
  555. package/template/themes/dogga/src/Header.tsx +239 -0
  556. package/template/themes/dogga/src/Layout.tsx +43 -0
  557. package/template/themes/dogga/src/Views.tsx +71 -0
  558. package/template/themes/dogga/src/blocks/CollectionBlocks.tsx +674 -0
  559. package/template/themes/dogga/src/blocks/ContentBlocks.tsx +307 -0
  560. package/template/themes/dogga/src/blocks/ConversionBlocks.tsx +571 -0
  561. package/template/themes/dogga/src/blocks/HeaderBlocks.tsx +289 -0
  562. package/template/themes/dogga/src/index.tsx +53 -0
  563. package/template/themes/dogga/src/kit.tsx +362 -0
  564. package/template/themes/dogga/src/site.ts +56 -0
  565. package/template/themes/dogga/src/styles.css +396 -0
  566. package/template/themes/mahdia/screenshot.jpg +0 -0
  567. package/template/themes/mahdia/snapify.theme.json +65 -0
  568. package/template/themes/mahdia/src/Brand.tsx +39 -0
  569. package/template/themes/mahdia/src/Footer.tsx +210 -0
  570. package/template/themes/mahdia/src/Header.tsx +291 -0
  571. package/template/themes/mahdia/src/Layout.tsx +44 -0
  572. package/template/themes/mahdia/src/Views.tsx +79 -0
  573. package/template/themes/mahdia/src/blocks/CollectionBlocks.tsx +674 -0
  574. package/template/themes/mahdia/src/blocks/ContentBlocks.tsx +307 -0
  575. package/template/themes/mahdia/src/blocks/ConversionBlocks.tsx +571 -0
  576. package/template/themes/mahdia/src/blocks/HeaderBlocks.tsx +289 -0
  577. package/template/themes/mahdia/src/index.tsx +53 -0
  578. package/template/themes/mahdia/src/kit.tsx +362 -0
  579. package/template/themes/mahdia/src/site.ts +75 -0
  580. package/template/themes/mahdia/src/styles.css +446 -0
  581. package/template/themes/preveris/screenshot.jpg +0 -0
  582. package/template/themes/preveris/snapify.theme.json +126 -0
  583. package/template/themes/preveris/src/Brand.tsx +44 -0
  584. package/template/themes/preveris/src/Footer.tsx +202 -0
  585. package/template/themes/preveris/src/Header.tsx +239 -0
  586. package/template/themes/preveris/src/Layout.tsx +42 -0
  587. package/template/themes/preveris/src/Views.tsx +71 -0
  588. package/template/themes/preveris/src/blocks/CollectionBlocks.tsx +678 -0
  589. package/template/themes/preveris/src/blocks/ContentBlocks.tsx +307 -0
  590. package/template/themes/preveris/src/blocks/ConversionBlocks.tsx +571 -0
  591. package/template/themes/preveris/src/blocks/HeaderBlocks.tsx +299 -0
  592. package/template/themes/preveris/src/index.tsx +53 -0
  593. package/template/themes/preveris/src/kit.tsx +365 -0
  594. package/template/themes/preveris/src/site.ts +79 -0
  595. package/template/themes/preveris/src/styles.css +446 -0
  596. package/template/themes/sidibou/screenshot.jpg +0 -0
  597. package/template/themes/sidibou/snapify.theme.json +21 -0
  598. package/template/themes/sidibou/src/CeeSimulator.tsx +247 -0
  599. package/template/themes/sidibou/src/Footer.tsx +203 -0
  600. package/template/themes/sidibou/src/Header.tsx +324 -0
  601. package/template/themes/sidibou/src/Layout.tsx +53 -0
  602. package/template/themes/sidibou/src/NotFound.tsx +58 -0
  603. package/template/themes/sidibou/src/Post.tsx +26 -0
  604. package/template/themes/sidibou/src/SnapifyLogo.tsx +44 -0
  605. package/template/themes/sidibou/src/blocks/CollectionBlocks.tsx +711 -0
  606. package/template/themes/sidibou/src/blocks/ContentBlocks.tsx +318 -0
  607. package/template/themes/sidibou/src/blocks/ConversionBlocks.tsx +559 -0
  608. package/template/themes/sidibou/src/blocks/HeaderBlocks.tsx +341 -0
  609. package/template/themes/sidibou/src/index.tsx +54 -0
  610. package/template/themes/sidibou/src/kit.tsx +398 -0
  611. package/template/themes/sidibou/src/site.ts +28 -0
  612. package/template/themes/sidibou/src/styles.css +317 -0
  613. package/template/tsconfig.json +33 -0
  614. package/template/vite.config.ts +42 -0
@@ -0,0 +1,1122 @@
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
+ * Gestionnaire des modules : installation, vérifications (signature NOVALABS, licence,
9
+ * intégrité des fichiers, compatibilité) et cycle de vie du code serveur.
10
+ *
11
+ * Modèle de sécurité : seul un paquet signé par une clé de `trust.ts` s'installe (sauf mode
12
+ * développeur). Le manifeste signé liste l'empreinte de chaque fichier ; elles sont revérifiées
13
+ * au démarrage et avant chaque chargement. Le code serveur d'un module s'exécute dans le
14
+ * processus du CMS et n'obtient que les capacités du contexte, filtrées par ses permissions.
15
+ */
16
+ import crypto from 'crypto';
17
+ import fs from 'fs/promises';
18
+ import path from 'path';
19
+ import { fileURLToPath, pathToFileURL } from 'url';
20
+ import { and, count, eq, inArray } from 'drizzle-orm';
21
+ import { db } from '../db/client';
22
+ import * as t from '../db/schema';
23
+ import { getDataPaths, loadSnapshot } from '../cms/store';
24
+ import type { CmsSnapshot } from '../cms/types';
25
+ import { decodeImage, ImageError, removeImage, saveImage } from '../media/images';
26
+ import {
27
+ packageKind,
28
+ SCREENSHOT_FILES,
29
+ SNAPIFY_MODULE_API_VERSION,
30
+ type ModuleCookie,
31
+ type ModuleHttpMethod,
32
+ type ModuleReply,
33
+ type ModuleRequest,
34
+ type ModuleRouteHandler,
35
+ type ModuleStatusReport,
36
+ type PackagedManifest,
37
+ type ServerContext,
38
+ type ServerModule,
39
+ type Signature,
40
+ type SignedLicense,
41
+ } from '../../sdk/index';
42
+ import { renderEmail, sendMail } from '../mail';
43
+ import { createCollection, deleteModuleRecords } from './records';
44
+ import { checkLicensePayload, decodeLicenseKey, decodePackage, domainAllowed, PackageError, satisfies, sha256, verify, compareVersions } from '../../sdk/package';
45
+ import { publicSettings, validateSettings } from '../../sdk/settings';
46
+ import { TRUSTED_KEYS } from './trust';
47
+ import { LEGACY_KEYS, OFFICIAL_MODULES } from './legacy';
48
+ import { localPackage } from './marketplace';
49
+ import type {
50
+ InstalledModuleDetail,
51
+ InstalledModuleInfo,
52
+ InstalledThemeDetail,
53
+ InstalledThemeInfo,
54
+ ModuleLicenseInfo,
55
+ ModuleSignatureInfo,
56
+ ModuleStatusCode,
57
+ PublicModuleDescriptor,
58
+ PublicThemeDescriptor,
59
+ StagedPackageInfo,
60
+ } from './types';
61
+
62
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
63
+ const MODULES_DIR = path.resolve(__dirname, '../../data/modules');
64
+ const BOOTSTRAP_KEY = 'modules.bootstrap';
65
+ const THEMES_BOOTSTRAP_KEY = 'themes.bootstrap';
66
+ /** Thème par défaut : installé au premier démarrage et utilisé en secours si le thème actif est bloqué. */
67
+ export const DEFAULT_THEME_ID = 'novalabs.sidibou';
68
+ const STAGE_TTL_MS = 15 * 60 * 1000;
69
+ const ACTIVATE_TIMEOUT_MS = 10_000;
70
+ /** Routes admin modules (génération IA, pipelines…) — au-delà → « délai dépassé ». */
71
+ const HANDLER_TIMEOUT_MS = 120_000;
72
+ const STORAGE_MAX_VALUE = 64 * 1024;
73
+ const FILES_MAX_BYTES = 2 * 1024 * 1024;
74
+ const STORAGE_MAX_KEYS = 1000;
75
+ const RATE_LIMIT_PER_MIN = 120;
76
+ const PUBLIC_ASSETS: readonly string[] = ['public.js', 'public.css', 'admin.js', 'admin.css', ...SCREENSHOT_FILES];
77
+ const ADMIN_ASSETS: readonly string[] = ['admin.js', 'admin.css'];
78
+
79
+ export const isDevMode = () => process.env.SNAPIFY_MODULES_DEV_MODE === 'true';
80
+
81
+ type Row = typeof t.modules.$inferSelect;
82
+
83
+ interface RouteEntry {
84
+ method: ModuleHttpMethod;
85
+ segments: string[];
86
+ handler: ModuleRouteHandler;
87
+ }
88
+
89
+ interface Runtime {
90
+ routes: RouteEntry[];
91
+ adminRoutes: RouteEntry[];
92
+ services: object | null;
93
+ status?: () => ModuleStatusReport | Promise<ModuleStatusReport>;
94
+ deactivate?: () => void;
95
+ }
96
+
97
+ interface ModuleState {
98
+ row: Row;
99
+ signature: ModuleSignatureInfo;
100
+ licenseSignatureValid: boolean;
101
+ integrity: 'ok' | 'tampered';
102
+ assets: Map<string, Buffer>;
103
+ runtime: Runtime | null;
104
+ runtimeError: string | null;
105
+ }
106
+
107
+ export class ModuleError extends PackageError {
108
+ constructor(public status: number, message: string) {
109
+ super(message);
110
+ }
111
+ }
112
+
113
+ const states = new Map<string, ModuleState>();
114
+ let site: CmsSnapshot | null = null;
115
+ let queue: Promise<unknown> = Promise.resolve();
116
+
117
+ /** Les opérations d'installation et de configuration sont sérialisées. */
118
+ function serialize<T>(fn: () => Promise<T>): Promise<T> {
119
+ const run = queue.then(fn);
120
+ queue = run.catch(() => undefined);
121
+ return run;
122
+ }
123
+
124
+ const moduleDir = (id: string, version: string) => path.join(MODULES_DIR, id, version);
125
+ const isTheme = (s: ModuleState) => s.row.kind === 'theme';
126
+
127
+ async function refreshSite() {
128
+ site = await loadSnapshot();
129
+ }
130
+
131
+ function siteHost(): string {
132
+ try {
133
+ return new URL(site?.settings.publicUrl || 'http://localhost').hostname;
134
+ } catch {
135
+ return 'localhost';
136
+ }
137
+ }
138
+
139
+ /* ------------------------------------------------------------------ */
140
+ /* Vérifications */
141
+ /* ------------------------------------------------------------------ */
142
+
143
+ function signatureInfo(manifest: PackagedManifest, signature: Signature | null | undefined): ModuleSignatureInfo {
144
+ const valid = verify(manifest, signature ?? undefined, TRUSTED_KEYS);
145
+ return { valid, kid: valid ? signature!.kid : undefined, devUnsigned: !valid && !signature && isDevMode() };
146
+ }
147
+
148
+ function evaluateLicense(
149
+ moduleId: string,
150
+ pricing: PackagedManifest['pricing'],
151
+ license: SignedLicense | null | undefined,
152
+ licenseSignatureValid: boolean,
153
+ sig: ModuleSignatureInfo
154
+ ): ModuleLicenseInfo {
155
+ if (!license) {
156
+ if (sig.devUnsigned) return { state: 'dev', message: 'Mode développeur : licence non vérifiée' };
157
+ return { state: 'missing', message: pricing === 'paid' ? 'Clé de licence requise pour activer ce module' : 'Licence absente du paquet' };
158
+ }
159
+ const p = license.payload;
160
+ if (!licenseSignatureValid || checkLicensePayload(p).length || p.moduleId !== moduleId) {
161
+ return { state: 'invalid', message: 'Licence invalide ou non émise par NOVALABS' };
162
+ }
163
+ const base = { licenseId: p.licenseId, licensee: p.licensee, plan: p.plan, domains: p.domains, expiresAt: p.expiresAt };
164
+ if (p.expiresAt && Date.parse(p.expiresAt) < Date.now()) {
165
+ return { ...base, state: 'expired', message: `Licence expirée le ${new Date(p.expiresAt).toLocaleDateString('fr-FR')}` };
166
+ }
167
+ const host = siteHost();
168
+ if (!domainAllowed(p.domains, host)) return { ...base, state: 'domain', message: `Licence non valable pour le domaine ${host}` };
169
+ return { ...base, state: 'valid', message: p.plan === 'free' ? 'Licence gratuite' : `Licence ${p.plan}` };
170
+ }
171
+
172
+ const licenseOf = (s: ModuleState) => evaluateLicense(s.row.id, s.row.manifest.pricing, s.row.license, s.licenseSignatureValid, s.signature);
173
+
174
+ /** Statut hors erreur d'exécution : `active` signifie que le module peut fonctionner. */
175
+ function gateStatus(s: ModuleState, depth = 0): { code: ModuleStatusCode; message: string } {
176
+ if (!s.signature.valid && !s.signature.devUnsigned) {
177
+ return { code: 'unsigned', message: 'Signature absente ou invalide : module bloqué' };
178
+ }
179
+ if (s.integrity === 'tampered') return { code: 'tampered', message: 'Fichiers modifiés ou manquants depuis l’installation : module bloqué' };
180
+ if (!satisfies(SNAPIFY_MODULE_API_VERSION, s.row.manifest.engines.snapify)) {
181
+ return { code: 'incompatible', message: `Nécessite Snapify ${s.row.manifest.engines.snapify}` };
182
+ }
183
+ const lic = licenseOf(s);
184
+ if (lic.state !== 'valid' && lic.state !== 'dev') return { code: `license_${lic.state}` as ModuleStatusCode, message: lic.message };
185
+ if (!s.row.enabled) return { code: 'disabled', message: isTheme(s) ? 'Thème inactif' : 'Module désactivé' };
186
+ const missing = missingDependency(s, depth);
187
+ if (missing) return { code: 'dependency', message: missing };
188
+ return { code: 'active', message: s.signature.devUnsigned ? 'Actif (non signé, mode développeur)' : 'Actif' };
189
+ }
190
+
191
+ /** Première dépendance non satisfaite (absente, inactive ou de version incompatible), sinon `null`. */
192
+ function missingDependency(s: ModuleState, depth: number): string | null {
193
+ for (const [id, range] of Object.entries(s.row.manifest.requires ?? {})) {
194
+ const dep = states.get(id);
195
+ const name = dep?.row.manifest.name ?? id;
196
+ if (!dep || isTheme(dep)) return `Nécessite le module « ${name} » (${range}) : installez-le depuis la marketplace.`;
197
+ if (!satisfies(dep.row.version, range)) return `Nécessite « ${name} » ${range} (version installée : ${dep.row.version}).`;
198
+ if (depth > 5 || gateStatus(dep, depth + 1).code !== 'active') return `Nécessite le module « ${name} », actuellement inactif.`;
199
+ }
200
+ return null;
201
+ }
202
+
203
+ /** Vérifications passées (signature, intégrité, compatibilité, licence), qu'il soit activé ou non. */
204
+ function usable(s: ModuleState): boolean {
205
+ const code = gateStatus(s).code;
206
+ return code === 'active' || code === 'disabled' || code === 'dependency';
207
+ }
208
+
209
+ function statusOf(s: ModuleState) {
210
+ const gate = gateStatus(s);
211
+ if (gate.code === 'active' && s.runtimeError) return { code: 'error' as const, message: s.runtimeError };
212
+ return gate;
213
+ }
214
+
215
+ async function loadState(row: Row): Promise<ModuleState> {
216
+ const dir = moduleDir(row.id, row.version);
217
+ const assets = new Map<string, Buffer>();
218
+ let integrity: ModuleState['integrity'] = 'ok';
219
+ for (const [name, meta] of Object.entries(row.manifest.files)) {
220
+ const data = await fs.readFile(path.join(dir, name)).catch(() => null);
221
+ if (!data || sha256(data) !== meta.sha256) {
222
+ integrity = 'tampered';
223
+ continue;
224
+ }
225
+ if (PUBLIC_ASSETS.includes(name)) assets.set(name, data);
226
+ }
227
+ return {
228
+ row,
229
+ signature: signatureInfo(row.manifest, row.signature),
230
+ licenseSignatureValid: !!row.license && verify(row.license.payload, row.license.signature, TRUSTED_KEYS),
231
+ integrity,
232
+ assets,
233
+ runtime: null,
234
+ runtimeError: null,
235
+ };
236
+ }
237
+
238
+ /* ------------------------------------------------------------------ */
239
+ /* Exécution côté serveur */
240
+ /* ------------------------------------------------------------------ */
241
+
242
+ const exposedErrors = new WeakSet<Error>();
243
+ const replies = new WeakSet<object>();
244
+ const ROUTE_PATTERN = /^\/([a-z0-9:][a-z0-9/:_-]{0,99})?$/;
245
+ const STORAGE_KEY_PATTERN = /^[A-Za-z0-9:._-]{1,100}$/;
246
+ const COOKIE_NAME_PATTERN = /^sm_[a-z0-9_]{1,40}$/;
247
+ const locks = new Map<string, Promise<unknown>>();
248
+
249
+ const splitPath = (p: string) => p.split('/').filter(Boolean);
250
+
251
+ function matchRoute(routes: RouteEntry[], method: string, routePath: string): { handler: ModuleRouteHandler; params: Record<string, string> } | null {
252
+ const parts = splitPath(routePath);
253
+ for (const r of routes) {
254
+ if (r.method !== method || r.segments.length !== parts.length) continue;
255
+ const params: Record<string, string> = {};
256
+ let ok = true;
257
+ for (let i = 0; i < parts.length && ok; i++) {
258
+ const seg = r.segments[i];
259
+ if (seg.startsWith(':')) {
260
+ try {
261
+ params[seg.slice(1)] = decodeURIComponent(parts[i]).slice(0, 200);
262
+ } catch {
263
+ ok = false;
264
+ }
265
+ } else ok = seg === parts[i];
266
+ }
267
+ if (ok) return { handler: r.handler, params };
268
+ }
269
+ return null;
270
+ }
271
+
272
+ function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
273
+ let timer: NodeJS.Timeout;
274
+ return Promise.race([
275
+ promise.finally(() => clearTimeout(timer)),
276
+ new Promise<T>((_, reject) => {
277
+ timer = setTimeout(() => reject(new Error(message)), ms);
278
+ }),
279
+ ]);
280
+ }
281
+
282
+ function createContext(s: ModuleState, runtime: Runtime): ServerContext {
283
+ const { id, version } = s.row;
284
+ const perms = new Set<string>(s.row.manifest.permissions);
285
+ const need = (perm: string) => {
286
+ if (!perms.has(perm)) throw new Error(`[module ${id}] permission non déclarée : ${perm}`);
287
+ };
288
+ const prefix = `[module:${id}]`;
289
+ const addRoute = (list: RouteEntry[], perm: string, method: ModuleHttpMethod) => (p: string, handler: ModuleRouteHandler) => {
290
+ need(perm);
291
+ if (!ROUTE_PATTERN.test(p) || typeof handler !== 'function') throw new Error(`${prefix} route invalide : ${p}`);
292
+ list.push({ method, segments: splitPath(p), handler });
293
+ };
294
+ const route = (method: ModuleHttpMethod) => addRoute(runtime.routes, 'http:routes', method);
295
+ const adminRoute = (method: ModuleHttpMethod) => addRoute(runtime.adminRoutes, 'admin:routes', method);
296
+ const declared = new Set([...Object.keys(s.row.manifest.requires ?? {}), ...(s.row.manifest.uses ?? [])]);
297
+ const storageKey = (key: string) => {
298
+ need('storage');
299
+ if (!STORAGE_KEY_PATTERN.test(key)) throw new Error(`${prefix} clé de stockage invalide`);
300
+ return key;
301
+ };
302
+
303
+ return {
304
+ module: { id, version },
305
+ settings: () => structuredClone(states.get(id)?.row.settings ?? s.row.settings) as Record<string, unknown>,
306
+ log: {
307
+ info: (...a) => console.log(prefix, ...a),
308
+ warn: (...a) => console.warn(prefix, ...a),
309
+ error: (...a) => console.error(prefix, ...a),
310
+ },
311
+ http: {
312
+ get: route('GET'),
313
+ post: route('POST'),
314
+ reply: (body, opts = {}) => {
315
+ const cookies = opts.cookies ?? [];
316
+ if (cookies.length) need('http:cookies');
317
+ for (const c of cookies) {
318
+ if (!COOKIE_NAME_PATTERN.test(c?.name ?? '') || typeof c.value !== 'string' || c.value.length > 2000) throw new Error(`${prefix} cookie invalide : ${c?.name}`);
319
+ }
320
+ const status = Number(opts.status) >= 200 && Number(opts.status) < 600 ? Number(opts.status) : 200;
321
+ const reply: ModuleReply = { __snapifyReply: true, body, status, cookies };
322
+ replies.add(reply);
323
+ return reply;
324
+ },
325
+ },
326
+ db: {
327
+ collection: (name) => {
328
+ need('database');
329
+ return createCollection(id, name);
330
+ },
331
+ },
332
+ lock: async (key, fn) => {
333
+ const k = `${id}:${key}`;
334
+ const prev = locks.get(k) ?? Promise.resolve();
335
+ const run = prev.catch(() => undefined).then(fn);
336
+ const tail = run.catch(() => undefined);
337
+ locks.set(k, tail);
338
+ try {
339
+ return await run;
340
+ } finally {
341
+ if (locks.get(k) === tail) locks.delete(k);
342
+ }
343
+ },
344
+ mail: {
345
+ send: async (msg) => {
346
+ need('mail:send');
347
+ if (!msg || typeof msg.subject !== 'string' || typeof msg.html !== 'string') throw new Error(`${prefix} e-mail invalide`);
348
+ return sendMail({ to: msg.to, subject: msg.subject.slice(0, 200), html: msg.html, text: msg.text, replyTo: msg.replyTo, kind: 'notification' });
349
+ },
350
+ render: (content) => renderEmail(site!, content),
351
+ },
352
+ provide: (services) => {
353
+ if (!services || typeof services !== 'object') throw new Error(`${prefix} services invalides`);
354
+ runtime.services = services;
355
+ },
356
+ use: <T,>(moduleId: string) => {
357
+ if (!declared.has(moduleId)) throw new Error(`${prefix} module non déclaré dans requires/uses : ${moduleId}`);
358
+ const dep = states.get(moduleId);
359
+ if (!dep || gateStatus(dep).code !== 'active' || !dep.runtime?.services) return null;
360
+ return dep.runtime.services as T;
361
+ },
362
+ error: (status, message) => {
363
+ const err = Object.assign(new Error(String(message)), { status: Number(status) >= 400 && Number(status) < 600 ? Number(status) : 500 });
364
+ exposedErrors.add(err);
365
+ return err;
366
+ },
367
+ env: (name) => {
368
+ need(`env:${name}`);
369
+ return process.env[name];
370
+ },
371
+ fetch: async (url, init) => {
372
+ if (perms.has('net:outbound')) {
373
+ const { outboundFetch } = await import('./outbound');
374
+ return outboundFetch(String(url), init);
375
+ }
376
+ const target = new URL(url);
377
+ need(`net:${target.hostname}`);
378
+ if (target.protocol !== 'https:') throw new Error(`${prefix} seules les requêtes HTTPS sont autorisées`);
379
+ // 90 s : LLM / images (DALL·E, Imagen) dépassent souvent 30 s.
380
+ const signal = init?.signal ?? AbortSignal.timeout(90_000);
381
+ return fetch(target, { ...init, redirect: 'error', signal });
382
+ },
383
+ ftp: {
384
+ list: async (opts) => {
385
+ need('net:ftp');
386
+ const { ftpList } = await import('./outbound');
387
+ return ftpList(opts);
388
+ },
389
+ readText: async (opts) => {
390
+ need('net:ftp');
391
+ const { ftpReadText } = await import('./outbound');
392
+ return ftpReadText(opts);
393
+ },
394
+ },
395
+ site: () => {
396
+ need('site:read');
397
+ const g = site!.general;
398
+ return {
399
+ name: site!.branding.siteName,
400
+ legalName: g.legalName,
401
+ phone: g.phone,
402
+ phoneClean: g.phoneClean,
403
+ email: g.contactEmail,
404
+ slaHours: g.slaHours,
405
+ address: g.address,
406
+ publicUrl: site!.settings.publicUrl,
407
+ colors: { ...site!.branding.colors },
408
+ };
409
+ },
410
+ files: {
411
+ saveImage: async (image, opts = {}) => {
412
+ need('files');
413
+ const maxBytes = Math.min(Math.max(Number(opts.maxBytes) || FILES_MAX_BYTES, 1024), FILES_MAX_BYTES);
414
+ let decoded: { buf: Buffer; ext: string };
415
+ try {
416
+ decoded = decodeImage(image ?? {}, maxBytes);
417
+ } catch (e) {
418
+ if (!(e instanceof ImageError)) throw e;
419
+ const err = Object.assign(new Error(e.message), { status: e.status });
420
+ exposedErrors.add(err);
421
+ throw err;
422
+ }
423
+ return saveImage(`modules/${id}`, String(opts.prefix || 'image'), decoded.buf, decoded.ext);
424
+ },
425
+ delete: async (url) => {
426
+ need('files');
427
+ await removeImage(`modules/${id}`, url);
428
+ },
429
+ },
430
+ storage: {
431
+ get: async (key) => {
432
+ const [row] = await db
433
+ .select({ value: t.moduleStorage.value })
434
+ .from(t.moduleStorage)
435
+ .where(and(eq(t.moduleStorage.moduleId, id), eq(t.moduleStorage.key, storageKey(key))));
436
+ return row?.value as never;
437
+ },
438
+ set: async (key, value) => {
439
+ const k = storageKey(key);
440
+ const json = JSON.stringify(value ?? null);
441
+ if (Buffer.byteLength(json) > STORAGE_MAX_VALUE) throw new Error(`${prefix} valeur de stockage trop volumineuse (64 Ko max.)`);
442
+ const [{ n }] = await db.select({ n: count() }).from(t.moduleStorage).where(eq(t.moduleStorage.moduleId, id));
443
+ if (n >= STORAGE_MAX_KEYS) {
444
+ const [exists] = await db.select({ k: t.moduleStorage.key }).from(t.moduleStorage).where(and(eq(t.moduleStorage.moduleId, id), eq(t.moduleStorage.key, k)));
445
+ if (!exists) throw new Error(`${prefix} nombre maximal de clés atteint (${STORAGE_MAX_KEYS})`);
446
+ }
447
+ await db
448
+ .insert(t.moduleStorage)
449
+ .values({ moduleId: id, key: k, value: JSON.parse(json), updatedAt: new Date() })
450
+ .onConflictDoUpdate({ target: [t.moduleStorage.moduleId, t.moduleStorage.key], set: { value: JSON.parse(json), updatedAt: new Date() } });
451
+ },
452
+ delete: async (key) => {
453
+ await db.delete(t.moduleStorage).where(and(eq(t.moduleStorage.moduleId, id), eq(t.moduleStorage.key, storageKey(key))));
454
+ },
455
+ },
456
+ admin: {
457
+ status: (provider) => {
458
+ runtime.status = provider;
459
+ },
460
+ get: adminRoute('GET'),
461
+ post: adminRoute('POST'),
462
+ put: adminRoute('PUT'),
463
+ delete: adminRoute('DELETE'),
464
+ },
465
+ };
466
+ }
467
+
468
+ async function activateServer(s: ModuleState) {
469
+ const meta = s.row.manifest.files['server.mjs'];
470
+ if (!meta) return;
471
+ const file = path.join(moduleDir(s.row.id, s.row.version), 'server.mjs');
472
+ try {
473
+ const data = await fs.readFile(file);
474
+ if (sha256(data) !== meta.sha256) {
475
+ s.integrity = 'tampered';
476
+ return;
477
+ }
478
+ const mod = (await import(`${pathToFileURL(file).href}?sha=${meta.sha256}`)) as { default?: ServerModule };
479
+ if (!mod.default || typeof mod.default.activate !== 'function') throw new Error('le bundle serveur n’exporte pas de module (defineServerModule)');
480
+ const runtime: Runtime = { routes: [], adminRoutes: [], services: null };
481
+ const result = await withTimeout(Promise.resolve(mod.default.activate(createContext(s, runtime))), ACTIVATE_TIMEOUT_MS, 'activation trop longue');
482
+ if (typeof result === 'function') runtime.deactivate = result;
483
+ s.runtime = runtime;
484
+ s.runtimeError = null;
485
+ } catch (e) {
486
+ s.runtime = null;
487
+ s.runtimeError = `Erreur au démarrage du module : ${e instanceof Error ? e.message : String(e)}`;
488
+ console.error(`[modules] ${s.row.id}`, e);
489
+ }
490
+ }
491
+
492
+ function deactivateServer(s: ModuleState) {
493
+ if (!s.runtime) return;
494
+ try {
495
+ s.runtime.deactivate?.();
496
+ } catch (e) {
497
+ console.error(`[modules] ${s.row.id} : erreur à la désactivation`, e);
498
+ }
499
+ s.runtime = null;
500
+ }
501
+
502
+ /** Démarre ou arrête le code serveur selon le statut courant (licence expirée, désactivation…). */
503
+ async function reconcile(s: ModuleState) {
504
+ const shouldRun = gateStatus(s).code === 'active' && !!s.row.manifest.entries.server;
505
+ if (shouldRun && !s.runtime && !s.runtimeError) await activateServer(s);
506
+ if (!shouldRun) deactivateServer(s);
507
+ }
508
+
509
+ /** Modules triés pour que chaque dépendance démarre avant les modules qui l'utilisent. */
510
+ function inDependencyOrder(): ModuleState[] {
511
+ const out: ModuleState[] = [];
512
+ const seen = new Set<string>();
513
+ const visit = (s: ModuleState, depth: number) => {
514
+ if (seen.has(s.row.id) || depth > 10) return;
515
+ seen.add(s.row.id);
516
+ const m = s.row.manifest;
517
+ for (const dep of [...Object.keys(m.requires ?? {}), ...(m.uses ?? [])]) {
518
+ const d = states.get(dep);
519
+ if (d) visit(d, depth + 1);
520
+ }
521
+ out.push(s);
522
+ };
523
+ for (const s of states.values()) visit(s, 0);
524
+ return out;
525
+ }
526
+
527
+ export async function refreshModules() {
528
+ await refreshSite();
529
+ const ordered = inDependencyOrder();
530
+ // Arrêts d'abord (dépendants en premier), puis démarrages dans l'ordre des dépendances.
531
+ for (const s of [...ordered].reverse()) if (gateStatus(s).code !== 'active') deactivateServer(s);
532
+ for (const s of ordered) await reconcile(s);
533
+ }
534
+
535
+ /* ------------------------------------------------------------------ */
536
+ /* Installation */
537
+ /* ------------------------------------------------------------------ */
538
+
539
+ interface Staged {
540
+ manifest: PackagedManifest;
541
+ files: Map<string, Buffer>;
542
+ signature?: Signature;
543
+ license?: SignedLicense;
544
+ source: Row['source'];
545
+ initialSettings?: Record<string, unknown>;
546
+ initialEnabled?: boolean;
547
+ expires: number;
548
+ }
549
+
550
+ const staged = new Map<string, Staged>();
551
+
552
+ function inspect(entry: Staged, token: string): StagedPackageInfo {
553
+ const m = entry.manifest;
554
+ const sig = signatureInfo(m, entry.signature);
555
+ const existing = states.get(m.id);
556
+ const license = entry.license ?? (existing?.row.license || undefined);
557
+ const licenseValid = !!license && verify(license.payload, license.signature, TRUSTED_KEYS);
558
+ const lic = evaluateLicense(m.id, m.pricing, license, licenseValid, sig);
559
+ const compatible = satisfies(SNAPIFY_MODULE_API_VERSION, m.engines.snapify);
560
+ const blockers: string[] = [];
561
+ if (!sig.valid && !sig.devUnsigned) {
562
+ blockers.push(
563
+ entry.signature
564
+ ? 'Signature invalide : le paquet a été modifié ou n’a pas été émis par NOVALABS.'
565
+ : 'Paquet non signé : seuls les modules signés par NOVALABS peuvent être installés.'
566
+ );
567
+ }
568
+ if (!compatible) blockers.push(`Ce module nécessite Snapify ${m.engines.snapify} (API actuelle ${SNAPIFY_MODULE_API_VERSION}).`);
569
+ if (entry.license && lic.state !== 'valid' && lic.state !== 'dev') blockers.push(`Licence fournie refusée : ${lic.message}.`);
570
+ if (existing && existing.row.kind !== packageKind(m)) blockers.push(`L’identifiant ${m.id} est déjà utilisé par un ${existing.row.kind === 'theme' ? 'thème' : 'module'} installé.`);
571
+ const shot = m.screenshot ? entry.files.get(m.screenshot) : undefined;
572
+ const mime = m.screenshot?.endsWith('.png') ? 'image/png' : m.screenshot?.endsWith('.webp') ? 'image/webp' : 'image/jpeg';
573
+ const installedVersion = existing?.row.version ?? null;
574
+ const cmp = installedVersion ? compareVersions(m.version, installedVersion) : 1;
575
+ return {
576
+ token,
577
+ manifest: {
578
+ id: m.id,
579
+ kind: packageKind(m),
580
+ name: m.name,
581
+ version: m.version,
582
+ description: m.description,
583
+ author: m.author,
584
+ pricing: m.pricing,
585
+ category: m.category,
586
+ icon: m.icon,
587
+ permissions: m.permissions,
588
+ slots: m.slots,
589
+ engines: m.engines,
590
+ hasServer: !!m.entries.server,
591
+ settingsCount: m.settings.length,
592
+ ...(shot ? { screenshot: `data:${mime};base64,${shot.toString('base64')}` } : {}),
593
+ },
594
+ signature: sig,
595
+ license: lic,
596
+ compatible,
597
+ installedVersion,
598
+ action: !installedVersion ? 'install' : cmp > 0 ? 'update' : cmp === 0 ? 'reinstall' : 'downgrade',
599
+ blockers,
600
+ };
601
+ }
602
+
603
+ /** Décode et vérifie un paquet, puis le garde en attente de confirmation. */
604
+ export function stagePackage(
605
+ buf: Buffer,
606
+ source: Row['source'],
607
+ initial?: { settings?: Record<string, unknown>; enabled?: boolean }
608
+ ): StagedPackageInfo {
609
+ const { pkg, files } = decodePackage(buf);
610
+ const now = Date.now();
611
+ for (const [k, v] of staged) if (v.expires < now) staged.delete(k);
612
+ while (staged.size >= 10) staged.delete(staged.keys().next().value!);
613
+ const token = crypto.randomBytes(18).toString('base64url');
614
+ const entry: Staged = {
615
+ manifest: pkg.manifest,
616
+ files,
617
+ signature: pkg.signature,
618
+ license: pkg.license,
619
+ source,
620
+ initialSettings: initial?.settings,
621
+ initialEnabled: initial?.enabled,
622
+ expires: now + STAGE_TTL_MS,
623
+ };
624
+ staged.set(token, entry);
625
+ return inspect(entry, token);
626
+ }
627
+
628
+ export function stagedKind(token: string): 'theme' | 'module' | null {
629
+ const entry = staged.get(token);
630
+ return entry ? packageKind(entry.manifest) : null;
631
+ }
632
+
633
+ export function installStaged(token: string): Promise<InstalledModuleInfo> {
634
+ return serialize(async () => {
635
+ const entry = staged.get(token);
636
+ if (!entry || entry.expires < Date.now()) throw new ModuleError(410, 'Ce paquet n’est plus disponible : recommencez l’opération.');
637
+ const check = inspect(entry, token);
638
+ if (check.blockers.length) throw new ModuleError(422, check.blockers.join(' '));
639
+ staged.delete(token);
640
+ await refreshSite();
641
+
642
+ const m = entry.manifest;
643
+ const existing = states.get(m.id);
644
+ const finalDir = moduleDir(m.id, m.version);
645
+ const tmp = path.join(MODULES_DIR, `.tmp-${crypto.randomBytes(6).toString('hex')}`);
646
+ await fs.mkdir(tmp, { recursive: true });
647
+ try {
648
+ for (const [name, data] of entry.files) await fs.writeFile(path.join(tmp, name), data, { mode: 0o644 });
649
+ if (existing) deactivateServer(existing);
650
+ await fs.mkdir(path.dirname(finalDir), { recursive: true });
651
+ await fs.rm(finalDir, { recursive: true, force: true });
652
+ await fs.rename(tmp, finalDir);
653
+ } catch (e) {
654
+ await fs.rm(tmp, { recursive: true, force: true });
655
+ throw e;
656
+ }
657
+
658
+ const settings = validateSettings(m.settings, existing?.row.settings ?? entry.initialSettings ?? {}).value;
659
+ const kind = packageKind(m);
660
+ // Un nouveau thème s'installe inactif, sauf s'il n'y a encore aucun thème actif.
661
+ const defaultEnabled = kind === 'theme' ? !activeThemeState() : true;
662
+ const values = {
663
+ kind,
664
+ version: m.version,
665
+ enabled: existing ? existing.row.enabled : entry.initialEnabled ?? defaultEnabled,
666
+ manifest: m,
667
+ signature: entry.signature ?? null,
668
+ license: entry.license ?? existing?.row.license ?? null,
669
+ settings,
670
+ source: entry.source,
671
+ updatedAt: new Date(),
672
+ };
673
+ const [row] = await db
674
+ .insert(t.modules)
675
+ .values({ id: m.id, ...values, installedAt: new Date() })
676
+ .onConflictDoUpdate({ target: t.modules.id, set: values })
677
+ .returning();
678
+
679
+ for (const dir of await fs.readdir(path.join(MODULES_DIR, m.id))) {
680
+ if (dir !== m.version) await fs.rm(path.join(MODULES_DIR, m.id, dir), { recursive: true, force: true });
681
+ }
682
+ const state = await loadState(row);
683
+ states.set(m.id, state);
684
+ await refreshModules();
685
+ console.log(`[modules] ${kind === 'theme' ? 'thème ' : ''}${m.id} ${m.version} installé (${entry.source})`);
686
+ return info(state);
687
+ });
688
+ }
689
+
690
+ function requireState(id: string): ModuleState {
691
+ const s = states.get(id);
692
+ if (!s) throw new ModuleError(404, 'Module non installé');
693
+ return s;
694
+ }
695
+
696
+ async function saveRow(s: ModuleState, patch: Partial<Pick<Row, 'enabled' | 'settings' | 'license'>>) {
697
+ const [row] = await db
698
+ .update(t.modules)
699
+ .set({ ...patch, updatedAt: new Date() })
700
+ .where(eq(t.modules.id, s.row.id))
701
+ .returning();
702
+ s.row = row;
703
+ }
704
+
705
+ export function uninstallModule(id: string): Promise<void> {
706
+ return serialize(async () => {
707
+ const s = requireState(id);
708
+ if (isTheme(s) && s.row.enabled) throw new ModuleError(409, 'Ce thème est actif : activez un autre thème avant de le supprimer.');
709
+ deactivateServer(s);
710
+ await db.delete(t.moduleStorage).where(eq(t.moduleStorage.moduleId, id));
711
+ await deleteModuleRecords(id);
712
+ await db.delete(t.modules).where(eq(t.modules.id, id));
713
+ states.delete(id);
714
+ await fs.rm(path.join(MODULES_DIR, id), { recursive: true, force: true });
715
+ await fs.rm(path.join(getDataPaths().UPLOADS_DIR, 'modules', id), { recursive: true, force: true });
716
+ await refreshModules();
717
+ console.log(`[modules] ${id} désinstallé`);
718
+ });
719
+ }
720
+
721
+ export function setModuleEnabled(id: string, enabled: boolean): Promise<InstalledModuleInfo> {
722
+ return serialize(async () => {
723
+ const s = requireState(id);
724
+ if (isTheme(s)) throw new ModuleError(400, 'Un thème s’active depuis Apparence > Thèmes.');
725
+ await saveRow(s, { enabled });
726
+ s.runtimeError = null;
727
+ await refreshModules();
728
+ return info(s);
729
+ });
730
+ }
731
+
732
+ export function updateModuleSettings(id: string, input: unknown): Promise<{ settings?: Record<string, unknown>; errors?: Record<string, string> }> {
733
+ return serialize(async () => {
734
+ const s = requireState(id);
735
+ const { value, errors } = validateSettings(s.row.manifest.settings, input, s.row.settings);
736
+ if (Object.keys(errors).length) return { errors };
737
+ await saveRow(s, { settings: value });
738
+ return { settings: value };
739
+ });
740
+ }
741
+
742
+ export function setModuleLicense(id: string, key: string): Promise<InstalledModuleInfo> {
743
+ return serialize(async () => {
744
+ const s = requireState(id);
745
+ const license = decodeLicenseKey(key);
746
+ const valid = verify(license.payload, license.signature, TRUSTED_KEYS);
747
+ await refreshSite();
748
+ const lic = evaluateLicense(id, s.row.manifest.pricing, license, valid, s.signature);
749
+ if (license.payload?.moduleId !== id) throw new ModuleError(400, 'Cette licence concerne un autre module');
750
+ if (lic.state !== 'valid') throw new ModuleError(400, lic.message);
751
+ await saveRow(s, { license });
752
+ s.licenseSignatureValid = true;
753
+ s.runtimeError = null;
754
+ await refreshModules();
755
+ return info(s);
756
+ });
757
+ }
758
+
759
+ /* ------------------------------------------------------------------ */
760
+ /* Lecture */
761
+ /* ------------------------------------------------------------------ */
762
+
763
+ function info(s: ModuleState): InstalledModuleInfo {
764
+ const m = s.row.manifest;
765
+ return {
766
+ id: m.id,
767
+ kind: s.row.kind,
768
+ name: m.name,
769
+ version: m.version,
770
+ description: m.description,
771
+ author: m.author,
772
+ pricing: m.pricing,
773
+ category: m.category,
774
+ icon: m.icon,
775
+ permissions: m.permissions,
776
+ slots: m.slots,
777
+ enabled: s.row.enabled,
778
+ source: s.row.source,
779
+ installedAt: s.row.installedAt.toISOString(),
780
+ updatedAt: s.row.updatedAt.toISOString(),
781
+ status: statusOf(s),
782
+ signature: s.signature,
783
+ license: licenseOf(s),
784
+ hasSettings: m.settings.length > 0,
785
+ hasServer: !!m.entries.server,
786
+ routes: m.routes ?? [],
787
+ widgets: m.widgets ?? [],
788
+ adminPages: m.adminPages ?? [],
789
+ requires: m.requires ?? {},
790
+ admin:
791
+ s.assets.has('admin.js') && gateStatus(s).code === 'active'
792
+ ? { script: assetUrl(s, 'admin.js'), ...(s.assets.has('admin.css') ? { style: assetUrl(s, 'admin.css') } : {}) }
793
+ : null,
794
+ };
795
+ }
796
+
797
+ export async function listModules(): Promise<InstalledModuleInfo[]> {
798
+ await refreshModules();
799
+ return [...states.values()].filter((s) => !isTheme(s)).map(info).sort((a, b) => a.name.localeCompare(b.name, 'fr'));
800
+ }
801
+
802
+ export function installedVersions(): Map<string, string> {
803
+ return new Map([...states.values()].map((s) => [s.row.id, s.row.version]));
804
+ }
805
+
806
+ export async function moduleDetail(id: string): Promise<InstalledModuleDetail> {
807
+ await refreshModules();
808
+ const s = requireState(id);
809
+ if (isTheme(s)) throw new ModuleError(404, 'Module non installé');
810
+ let report: ModuleStatusReport | null = null;
811
+ if (s.runtime?.status) {
812
+ try {
813
+ report = await withTimeout(Promise.resolve(s.runtime.status()), 3000, 'délai dépassé');
814
+ } catch (e) {
815
+ report = { alerts: [{ tone: 'warning', text: `Indicateurs indisponibles : ${e instanceof Error ? e.message : 'erreur'}` }] };
816
+ }
817
+ }
818
+ return { ...info(s), settingsSchema: s.row.manifest.settings, settings: s.row.settings, report };
819
+ }
820
+
821
+ const assetUrl = (s: ModuleState, file: string) => `/_modules/${s.row.id}/${s.row.version}/${file}?h=${s.row.manifest.files[file].sha256.slice(0, 12)}`;
822
+
823
+ export async function publicModules(): Promise<PublicModuleDescriptor[]> {
824
+ await refreshSite();
825
+ return [...states.values()]
826
+ .filter((s) => !isTheme(s) && gateStatus(s).code === 'active' && !s.runtimeError && s.assets.has('public.js'))
827
+ .map((s) => ({
828
+ id: s.row.id,
829
+ version: s.row.version,
830
+ slots: s.row.manifest.slots,
831
+ routes: s.row.manifest.routes ?? [],
832
+ widgets: (s.row.manifest.widgets ?? []).map((w) => w.id),
833
+ script: assetUrl(s, 'public.js'),
834
+ ...(s.assets.has('public.css') ? { style: assetUrl(s, 'public.css') } : {}),
835
+ settings: publicSettings(s.row.manifest.settings, s.row.settings),
836
+ }));
837
+ }
838
+
839
+ /** Config publique reCAPTCHA (module hamdiayari.recaptcha) si actif et configuré. */
840
+ export function publicRecaptchaConfig(): { siteKey: string; version: 'v2' | 'v3'; action: string } | null {
841
+ const s = states.get('hamdiayari.recaptcha');
842
+ if (!s || isTheme(s) || gateStatus(s).code !== 'active') return null;
843
+ const st = s.row.settings as Record<string, unknown>;
844
+ if (st.enabled === false) return null;
845
+ const siteKey = typeof st.siteKey === 'string' ? st.siteKey.trim() : '';
846
+ const secretKey = typeof st.secretKey === 'string' ? st.secretKey.trim() : '';
847
+ if (!siteKey || !secretKey) return null;
848
+ return {
849
+ siteKey,
850
+ version: st.version === 'v3' ? 'v3' : 'v2',
851
+ action: typeof st.v3Action === 'string' && st.v3Action.trim() ? st.v3Action.trim().slice(0, 40) : 'contact',
852
+ };
853
+ }
854
+
855
+ /** Vérifie un jeton reCAPTCHA auprès de Google (module actif requis). */
856
+ export async function verifyRecaptchaToken(token: string, remoteip?: string): Promise<{ ok: true } | { ok: false; error: string }> {
857
+ const s = states.get('hamdiayari.recaptcha');
858
+ if (!s || isTheme(s) || gateStatus(s).code !== 'active') return { ok: true };
859
+ const st = s.row.settings as Record<string, unknown>;
860
+ if (st.enabled === false) return { ok: true };
861
+ const secret = typeof st.secretKey === 'string' ? st.secretKey.trim() : '';
862
+ if (!secret) return { ok: false, error: 'reCAPTCHA non configuré (clé secrète manquante).' };
863
+ if (!token?.trim()) return { ok: false, error: 'Cochez la case « Je ne suis pas un robot ».' };
864
+
865
+ const body = new URLSearchParams({ secret, response: token.trim() });
866
+ if (remoteip) body.set('remoteip', remoteip.slice(0, 64));
867
+ try {
868
+ const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
869
+ method: 'POST',
870
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
871
+ body,
872
+ });
873
+ const data = (await res.json()) as { success?: boolean; score?: number };
874
+ if (!data.success) return { ok: false, error: 'Vérification anti-robot échouée. Réessayez.' };
875
+ if (st.version === 'v3') {
876
+ const min = Number(st.v3MinScore);
877
+ const threshold = Number.isFinite(min) ? Math.min(1, Math.max(0, min)) : 0.5;
878
+ if (Number(data.score ?? 0) < threshold) {
879
+ return { ok: false, error: 'Score anti-robot insuffisant. Réessayez ou contactez-nous par téléphone.' };
880
+ }
881
+ }
882
+ return { ok: true };
883
+ } catch {
884
+ return { ok: false, error: 'Impossible de joindre Google reCAPTCHA. Réessayez plus tard.' };
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Fichier public servi depuis la copie vérifiée en mémoire : modules actifs, thèmes vérifiés
890
+ * (même inactifs, pour l'aperçu) et captures d'écran des thèmes intacts.
891
+ */
892
+ export function moduleAsset(id: string, version: string, file: string): Buffer | null {
893
+ const s = states.get(id);
894
+ if (!s || s.row.version !== version || !PUBLIC_ASSETS.includes(file)) return null;
895
+ const code = gateStatus(s).code;
896
+ const shot = (SCREENSHOT_FILES as readonly string[]).includes(file);
897
+ const allowed = shot ? isTheme(s) && code !== 'unsigned' && code !== 'tampered' : isTheme(s) ? usable(s) && !ADMIN_ASSETS.includes(file) : code === 'active';
898
+ return allowed ? s.assets.get(file) ?? null : null;
899
+ }
900
+
901
+ /* ------------------------------------------------------------------ */
902
+ /* Thèmes */
903
+ /* ------------------------------------------------------------------ */
904
+
905
+ function activeThemeState(): ModuleState | undefined {
906
+ return [...states.values()].find((s) => isTheme(s) && s.row.enabled);
907
+ }
908
+
909
+ function themeDescriptor(s: ModuleState): PublicThemeDescriptor | null {
910
+ if (!usable(s) || !s.assets.has('public.js')) return null;
911
+ return {
912
+ id: s.row.id,
913
+ version: s.row.version,
914
+ script: assetUrl(s, 'public.js'),
915
+ ...(s.assets.has('public.css') ? { style: assetUrl(s, 'public.css') } : {}),
916
+ // Les options d'un thème sont toutes des choix de présentation : elles sont publiques.
917
+ settings: structuredClone(s.row.settings),
918
+ };
919
+ }
920
+
921
+ function themeInfo(s: ModuleState): InstalledThemeInfo {
922
+ const shot = SCREENSHOT_FILES.find((f) => s.assets.has(f));
923
+ return { ...info(s), active: s.row.enabled, ...(shot ? { screenshot: assetUrl(s, shot) } : {}) };
924
+ }
925
+
926
+ /** Thème appliqué au site : le thème actif, ou le thème par défaut s'il est bloqué. */
927
+ export async function publicTheme(): Promise<PublicThemeDescriptor | null> {
928
+ await refreshSite();
929
+ const active = activeThemeState();
930
+ const current = active && gateStatus(active).code === 'active' ? themeDescriptor(active) : null;
931
+ if (current) return current;
932
+ const fallback = states.get(DEFAULT_THEME_ID);
933
+ return fallback && isTheme(fallback) ? themeDescriptor(fallback) : null;
934
+ }
935
+
936
+ export async function listThemes(): Promise<InstalledThemeInfo[]> {
937
+ await refreshModules();
938
+ return [...states.values()]
939
+ .filter(isTheme)
940
+ .map(themeInfo)
941
+ .sort((a, b) => Number(b.active) - Number(a.active) || a.name.localeCompare(b.name, 'fr'));
942
+ }
943
+
944
+ function requireTheme(id: string): ModuleState {
945
+ const s = states.get(id);
946
+ if (!s || !isTheme(s)) throw new ModuleError(404, 'Thème non installé');
947
+ return s;
948
+ }
949
+
950
+ export async function themeDetail(id: string): Promise<InstalledThemeDetail> {
951
+ await refreshModules();
952
+ const s = requireTheme(id);
953
+ return { ...themeInfo(s), settingsSchema: s.row.manifest.settings, settings: s.row.settings, descriptor: themeDescriptor(s) };
954
+ }
955
+
956
+ export function activateTheme(id: string): Promise<InstalledThemeInfo> {
957
+ return serialize(async () => {
958
+ await refreshSite();
959
+ const s = requireTheme(id);
960
+ if (!usable(s)) throw new ModuleError(422, `Ce thème ne peut pas être activé : ${gateStatus(s).message}`);
961
+ const previous = activeThemeState();
962
+ const previousId = previous && previous !== s ? previous.row.id : null;
963
+ for (const other of states.values()) {
964
+ if (isTheme(other) && other !== s && other.row.enabled) await saveRow(other, { enabled: false });
965
+ }
966
+ if (!s.row.enabled) await saveRow(s, { enabled: true });
967
+ // Contenu CMS (accueil + menus) isolé par thème : sauvegarde l’ancien, restaure le nouveau
968
+ try {
969
+ const { switchThemeContent } = await import('../cms/themeContent');
970
+ await switchThemeContent(previousId, id);
971
+ } catch (e) {
972
+ console.error(`[themes] bascule contenu impossible pour ${id}`, e);
973
+ }
974
+ console.log(`[themes] thème actif : ${id}`);
975
+ return themeInfo(s);
976
+ });
977
+ }
978
+
979
+ /* ------------------------------------------------------------------ */
980
+ /* API publique des modules : /api/modules/<id>/<route> */
981
+ /* ------------------------------------------------------------------ */
982
+
983
+ const hits = new Map<string, { n: number; reset: number }>();
984
+
985
+ function rateLimited(key: string): boolean {
986
+ const now = Date.now();
987
+ if (hits.size > 10_000) for (const [k, v] of hits) if (v.reset < now) hits.delete(k);
988
+ const entry = hits.get(key);
989
+ if (!entry || entry.reset < now) {
990
+ hits.set(key, { n: 1, reset: now + 60_000 });
991
+ return false;
992
+ }
993
+ return ++entry.n > RATE_LIMIT_PER_MIN;
994
+ }
995
+
996
+ export interface ModuleResponse {
997
+ status: number;
998
+ body: unknown;
999
+ cookies?: ModuleCookie[];
1000
+ }
1001
+
1002
+ async function runHandler(id: string, handler: ModuleRouteHandler, req: ModuleRequest): Promise<ModuleResponse> {
1003
+ await refreshSite();
1004
+ try {
1005
+ const result = await withTimeout(Promise.resolve(handler(req)), HANDLER_TIMEOUT_MS, 'délai dépassé');
1006
+ if (result && typeof result === 'object' && replies.has(result)) {
1007
+ const r = result as ModuleReply;
1008
+ return { status: r.status, body: r.body ?? null, cookies: r.cookies };
1009
+ }
1010
+ return { status: 200, body: result ?? null };
1011
+ } catch (e) {
1012
+ if (e instanceof Error && exposedErrors.has(e)) return { status: (e as Error & { status: number }).status, body: { error: e.message } };
1013
+ console.error(`[module:${id}] erreur non gérée`, e);
1014
+ return { status: 500, body: { error: 'Erreur du module' } };
1015
+ }
1016
+ }
1017
+
1018
+ const filterCookies = (s: ModuleState, cookies: Record<string, string>) =>
1019
+ s.row.manifest.permissions.includes('http:cookies') ? Object.fromEntries(Object.entries(cookies).filter(([k]) => COOKIE_NAME_PATTERN.test(k))) : {};
1020
+
1021
+ export async function dispatchModuleRequest(
1022
+ id: string,
1023
+ method: string,
1024
+ routePath: string,
1025
+ req: Omit<ModuleRequest, 'method' | 'path' | 'params' | 'admin'>
1026
+ ): Promise<ModuleResponse> {
1027
+ const s = states.get(id);
1028
+ if (!s || gateStatus(s).code !== 'active' || !s.runtime) return { status: 404, body: { error: 'Module indisponible' } };
1029
+ if (method !== 'GET' && method !== 'POST') return { status: 405, body: { error: 'Méthode non autorisée' } };
1030
+ const match = matchRoute(s.runtime.routes, method, routePath);
1031
+ if (!match) return { status: 404, body: { error: 'Route inconnue' } };
1032
+ if (rateLimited(`${id}|${req.ip}`)) return { status: 429, body: { error: 'Trop de requêtes, réessayez dans une minute.' } };
1033
+ return runHandler(id, match.handler, { ...req, cookies: filterCookies(s, req.cookies), method, path: routePath, params: match.params });
1034
+ }
1035
+
1036
+ /** Routes d'administration d'un module (/api/admin/modules/<id>/api/…), après authentification. */
1037
+ export async function dispatchModuleAdminRequest(
1038
+ id: string,
1039
+ method: string,
1040
+ routePath: string,
1041
+ req: Omit<ModuleRequest, 'method' | 'path' | 'params' | 'cookies'> & { admin: NonNullable<ModuleRequest['admin']> }
1042
+ ): Promise<ModuleResponse> {
1043
+ const s = states.get(id);
1044
+ if (!s || isTheme(s) || gateStatus(s).code !== 'active' || !s.runtime) return { status: 404, body: { error: 'Module indisponible' } };
1045
+ if (!['GET', 'POST', 'PUT', 'DELETE'].includes(method)) return { status: 405, body: { error: 'Méthode non autorisée' } };
1046
+ const match = matchRoute(s.runtime.adminRoutes, method, routePath);
1047
+ if (!match) return { status: 404, body: { error: 'Route inconnue' } };
1048
+ const res = await runHandler(id, match.handler, { ...req, cookies: {}, method: method as ModuleHttpMethod, path: routePath, params: match.params });
1049
+ return { status: res.status, body: res.body };
1050
+ }
1051
+
1052
+ /* ------------------------------------------------------------------ */
1053
+ /* Démarrage */
1054
+ /* ------------------------------------------------------------------ */
1055
+
1056
+ /** Premier démarrage : installe les modules officiels et reprend les anciens réglages du cœur. */
1057
+ async function bootstrapOfficialModules() {
1058
+ const [done] = await db.select().from(t.siteSettings).where(eq(t.siteSettings.key, BOOTSTRAP_KEY));
1059
+ if (done) return;
1060
+ const legacyRows = await db.select().from(t.siteSettings).where(inArray(t.siteSettings.key, LEGACY_KEYS));
1061
+ const legacy = new Map(legacyRows.map((r) => [r.key, r.value as Record<string, unknown>]));
1062
+ const installed: string[] = [];
1063
+ for (const official of OFFICIAL_MODULES) {
1064
+ if (states.has(official.id)) continue;
1065
+ const buf = await localPackage(official.id);
1066
+ if (!buf) {
1067
+ console.error(`[modules] paquet officiel introuvable : ${official.id}`);
1068
+ return;
1069
+ }
1070
+ const old = legacy.get(official.legacyKey);
1071
+ const check = stagePackage(buf, 'marketplace', { settings: official.settings(old), enabled: official.enabled(old) });
1072
+ await installStaged(check.token);
1073
+ installed.push(official.id);
1074
+ }
1075
+ await db.insert(t.siteSettings).values({ key: BOOTSTRAP_KEY, value: { at: new Date().toISOString(), installed } }).onConflictDoNothing();
1076
+ if (legacyRows.length) await db.delete(t.siteSettings).where(inArray(t.siteSettings.key, LEGACY_KEYS));
1077
+ }
1078
+
1079
+ /** Premier démarrage : installe et active le thème par défaut. */
1080
+ async function bootstrapDefaultTheme() {
1081
+ const [done] = await db.select().from(t.siteSettings).where(eq(t.siteSettings.key, THEMES_BOOTSTRAP_KEY));
1082
+ if (done) return;
1083
+ if (!states.has(DEFAULT_THEME_ID)) {
1084
+ const buf = await localPackage(DEFAULT_THEME_ID);
1085
+ if (!buf) {
1086
+ console.error(`[themes] paquet du thème par défaut introuvable : ${DEFAULT_THEME_ID}`);
1087
+ return;
1088
+ }
1089
+ const check = stagePackage(buf, 'marketplace', { enabled: !activeThemeState() });
1090
+ await installStaged(check.token);
1091
+ }
1092
+ await db.insert(t.siteSettings).values({ key: THEMES_BOOTSTRAP_KEY, value: { at: new Date().toISOString(), installed: [DEFAULT_THEME_ID] } }).onConflictDoNothing();
1093
+ }
1094
+
1095
+ export async function initModules() {
1096
+ await fs.mkdir(MODULES_DIR, { recursive: true });
1097
+ await refreshSite();
1098
+ for (const row of await db.select().from(t.modules)) states.set(row.id, await loadState(row));
1099
+ try {
1100
+ await bootstrapOfficialModules();
1101
+ } catch (e) {
1102
+ console.error('[modules] installation initiale impossible', e);
1103
+ }
1104
+ try {
1105
+ await bootstrapDefaultTheme();
1106
+ } catch (e) {
1107
+ console.error('[themes] installation du thème par défaut impossible', e);
1108
+ }
1109
+ await refreshModules();
1110
+ try {
1111
+ const { ensureDefaultThemePacks } = await import('../cms/themeContent');
1112
+ await ensureDefaultThemePacks();
1113
+ } catch (e) {
1114
+ console.error('[themes] packs contenu par défaut impossibles', e);
1115
+ }
1116
+ for (const s of states.values()) {
1117
+ const st = statusOf(s);
1118
+ console.log(`[${isTheme(s) ? 'themes' : 'modules'}] ${s.row.id} ${s.row.version} : ${st.message}`);
1119
+ }
1120
+ if (isDevMode()) console.warn('[modules] MODE DÉVELOPPEUR : les modules non signés sont acceptés');
1121
+ setInterval(() => void refreshModules().catch((e) => console.error('[modules] vérification périodique', e)), 10 * 60 * 1000).unref();
1122
+ }