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,178 @@
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
+ * Catalogue de la marketplace : distant si MARKETPLACE_URL est défini (HTTPS uniquement),
9
+ * sinon le catalogue local du dossier marketplace/. L'empreinte SHA-256 annoncée par le
10
+ * catalogue est contrôlée au téléchargement ; l'authenticité repose sur la signature du paquet.
11
+ */
12
+ import fs from 'fs/promises';
13
+ import path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+ import { MODULE_ID_PATTERN, SEMVER_PATTERN } from '../../sdk/index';
16
+ import { MAX_PACKAGE_BYTES, PackageError, sha256 } from '../../sdk/package';
17
+ import type { CatalogEntry } from './types';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const LOCAL_DIR = path.resolve(__dirname, '../../marketplace');
21
+ const MAX_CATALOG_BYTES = 2 * 1024 * 1024;
22
+ const CACHE_MS = 5 * 60 * 1000;
23
+
24
+ export interface Catalog {
25
+ source: 'local' | 'remote';
26
+ url?: string;
27
+ error?: string;
28
+ modules: CatalogEntry[];
29
+ }
30
+
31
+ let cached: { at: number; catalog: Catalog } | null = null;
32
+
33
+ async function fetchLimited(url: URL, maxBytes: number, timeoutMs: number): Promise<Buffer> {
34
+ if (url.protocol !== 'https:') throw new PackageError('Seules les adresses HTTPS sont acceptées');
35
+ const res = await fetch(url, { redirect: 'error', signal: AbortSignal.timeout(timeoutMs) });
36
+ if (!res.ok) throw new PackageError(`Marketplace indisponible (HTTP ${res.status})`);
37
+ if (Number(res.headers.get('content-length') || 0) > maxBytes) throw new PackageError('Réponse trop volumineuse');
38
+ const buf = Buffer.from(await res.arrayBuffer());
39
+ if (buf.length > maxBytes) throw new PackageError('Réponse trop volumineuse');
40
+ return buf;
41
+ }
42
+
43
+ function sanitize(raw: unknown): CatalogEntry[] {
44
+ const list = (raw as { modules?: unknown })?.modules;
45
+ if (!Array.isArray(list)) throw new PackageError('Catalogue invalide');
46
+ return list.filter((e): e is CatalogEntry => {
47
+ const m = e as CatalogEntry;
48
+ return (
49
+ !!m &&
50
+ typeof m.id === 'string' &&
51
+ MODULE_ID_PATTERN.test(m.id) &&
52
+ typeof m.version === 'string' &&
53
+ SEMVER_PATTERN.test(m.version) &&
54
+ typeof m.name === 'string' &&
55
+ typeof m.package === 'string' &&
56
+ /^[a-f0-9]{64}$/.test(m.sha256) &&
57
+ Array.isArray(m.permissions) &&
58
+ typeof m.engines?.snapify === 'string' &&
59
+ (m.kind === undefined || m.kind === 'module' || m.kind === 'theme') &&
60
+ (m.screenshot === undefined || (typeof m.screenshot === 'string' && m.screenshot.length < 300)) &&
61
+ (m.gallery === undefined || (Array.isArray(m.gallery) && m.gallery.every((g) => typeof g === 'string' && g.length < 300))) &&
62
+ (m.priceMillimes === undefined || (typeof m.priceMillimes === 'number' && m.priceMillimes >= 0))
63
+ );
64
+ });
65
+ }
66
+
67
+ const SCREENSHOT_NAME = /^[a-z0-9.-]+\.(jpg|png|webp)$/;
68
+ const GALLERY_FILE = /^[a-z0-9.-]+\.(jpg|png|webp)$/;
69
+ const MODULE_DIR = /^[a-z0-9.-]+$/;
70
+
71
+ /** URL affichable d'une capture du catalogue : servie localement, ou HTTPS du catalogue distant. */
72
+ export function screenshotUrl(catalog: Catalog, entry: CatalogEntry): string | undefined {
73
+ if (!entry.screenshot) return undefined;
74
+ if (catalog.source === 'remote' && catalog.url && !catalog.error) {
75
+ try {
76
+ const url = new URL(entry.screenshot, catalog.url);
77
+ return url.protocol === 'https:' ? url.href : undefined;
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ const name = path.basename(entry.screenshot);
83
+ return SCREENSHOT_NAME.test(name) ? `/_marketplace/screenshots/${name}` : undefined;
84
+ }
85
+
86
+ /** URLs des captures galerie (catalogue local ou distant). */
87
+ export function galleryUrls(catalog: Catalog, entry: CatalogEntry): string[] {
88
+ if (!entry.gallery?.length) return [];
89
+ return entry.gallery
90
+ .map((g) => {
91
+ if (catalog.source === 'remote' && catalog.url && !catalog.error) {
92
+ try {
93
+ const url = new URL(g, catalog.url);
94
+ return url.protocol === 'https:' ? url.href : '';
95
+ } catch {
96
+ return '';
97
+ }
98
+ }
99
+ // gallery/<moduleId>/<file>
100
+ const parts = g.replace(/^\/+/, '').split('/');
101
+ if (parts[0] !== 'gallery' || parts.length !== 3) return '';
102
+ const [, moduleId, file] = parts;
103
+ if (!MODULE_DIR.test(moduleId) || !GALLERY_FILE.test(file)) return '';
104
+ return `/_marketplace/gallery/${moduleId}/${file}`;
105
+ })
106
+ .filter(Boolean);
107
+ }
108
+
109
+ /** Capture d'écran du catalogue local (`marketplace/screenshots/`). */
110
+ export async function localScreenshot(name: string): Promise<Buffer | null> {
111
+ if (!SCREENSHOT_NAME.test(name)) return null;
112
+ return fs.readFile(path.join(LOCAL_DIR, 'screenshots', name)).catch(() => null);
113
+ }
114
+
115
+ /** Image de galerie locale (`marketplace/gallery/<moduleId>/<file>`). */
116
+ export async function localGalleryImage(moduleId: string, file: string): Promise<Buffer | null> {
117
+ if (!MODULE_DIR.test(moduleId) || !GALLERY_FILE.test(file)) return null;
118
+ const abs = path.resolve(LOCAL_DIR, 'gallery', moduleId, file);
119
+ if (!abs.startsWith(path.join(LOCAL_DIR, 'gallery') + path.sep)) return null;
120
+ return fs.readFile(abs).catch(() => null);
121
+ }
122
+
123
+ async function localCatalog(): Promise<CatalogEntry[]> {
124
+ try {
125
+ return sanitize(JSON.parse(await fs.readFile(path.join(LOCAL_DIR, 'catalog.json'), 'utf8')));
126
+ } catch {
127
+ return [];
128
+ }
129
+ }
130
+
131
+ export async function getCatalog(force = false): Promise<Catalog> {
132
+ if (!force && cached && Date.now() - cached.at < CACHE_MS) return cached.catalog;
133
+ const remote = process.env.MARKETPLACE_URL?.trim();
134
+ let catalog: Catalog;
135
+ if (remote) {
136
+ try {
137
+ const buf = await fetchLimited(new URL(remote), MAX_CATALOG_BYTES, 15_000);
138
+ catalog = { source: 'remote', url: remote, modules: sanitize(JSON.parse(buf.toString('utf8'))) };
139
+ } catch (e) {
140
+ catalog = {
141
+ source: 'local',
142
+ url: remote,
143
+ error: `Catalogue distant inaccessible (${e instanceof Error ? e.message : 'erreur'}) : affichage du catalogue local.`,
144
+ modules: await localCatalog(),
145
+ };
146
+ }
147
+ } else {
148
+ catalog = { source: 'local', modules: await localCatalog() };
149
+ }
150
+ cached = { at: Date.now(), catalog };
151
+ return catalog;
152
+ }
153
+
154
+ export async function downloadPackage(id: string): Promise<Buffer> {
155
+ const catalog = await getCatalog();
156
+ const entry = catalog.modules.find((m) => m.id === id);
157
+ if (!entry) throw new PackageError('Élément absent du catalogue');
158
+ let buf: Buffer;
159
+ if (catalog.source === 'remote') {
160
+ buf = await fetchLimited(new URL(entry.package, catalog.url), MAX_PACKAGE_BYTES, 60_000);
161
+ } else {
162
+ const file = path.resolve(LOCAL_DIR, entry.package);
163
+ if (!file.startsWith(LOCAL_DIR + path.sep)) throw new PackageError('Chemin de paquet invalide');
164
+ buf = await fs.readFile(file);
165
+ }
166
+ if (sha256(buf) !== entry.sha256) throw new PackageError('Le paquet téléchargé ne correspond pas à l’empreinte du catalogue');
167
+ return buf;
168
+ }
169
+
170
+ /** Paquet du catalogue local (installation initiale des modules officiels, sans réseau). */
171
+ export async function localPackage(id: string): Promise<Buffer | null> {
172
+ const entry = (await localCatalog()).find((m) => m.id === id);
173
+ if (!entry) return null;
174
+ const file = path.resolve(LOCAL_DIR, entry.package);
175
+ if (!file.startsWith(LOCAL_DIR + path.sep)) return null;
176
+ const buf = await fs.readFile(file).catch(() => null);
177
+ return buf && sha256(buf) === entry.sha256 ? buf : null;
178
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Sorties réseau contrôlées pour les modules (`net:outbound`, `net:ftp`).
3
+ * Anti-SSRF : pas de localhost, IPs privées, metadata cloud, ni ports sensibles.
4
+ */
5
+ import dns from 'dns/promises';
6
+ import net from 'net';
7
+ import { Client } from 'basic-ftp';
8
+
9
+ const BLOCKED_PORTS = new Set([22, 23, 25, 445, 3389, 5432, 6379, 11211, 27017]);
10
+
11
+ function isPrivateIp(ip: string): boolean {
12
+ if (net.isIPv6(ip)) {
13
+ const v = ip.toLowerCase();
14
+ return (
15
+ v === '::1' ||
16
+ v.startsWith('fc') ||
17
+ v.startsWith('fd') ||
18
+ v.startsWith('fe80') ||
19
+ v === '::' ||
20
+ v.startsWith('::ffff:127.') ||
21
+ v.startsWith('::ffff:10.') ||
22
+ v.startsWith('::ffff:192.168.') ||
23
+ /^::ffff:172\.(1[6-9]|2\d|3[0-1])\./.test(v)
24
+ );
25
+ }
26
+ const m = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(ip);
27
+ if (!m) return true;
28
+ const a = Number(m[1]);
29
+ const b = Number(m[2]);
30
+ if (a === 10 || a === 127 || a === 0 || a === 100) return true;
31
+ if (a === 169 && b === 254) return true;
32
+ if (a === 172 && b >= 16 && b <= 31) return true;
33
+ if (a === 192 && b === 168) return true;
34
+ if (a >= 224) return true;
35
+ return false;
36
+ }
37
+
38
+ export async function assertSafeOutboundUrl(raw: string | URL, opts?: { allowHttp?: boolean }): Promise<URL> {
39
+ let u: URL;
40
+ try {
41
+ u = typeof raw === 'string' ? new URL(raw) : raw;
42
+ } catch {
43
+ throw new Error('Adresse invalide');
44
+ }
45
+ const allowHttp = opts?.allowHttp !== false;
46
+ if (u.protocol === 'https:') {
47
+ /* ok */
48
+ } else if (u.protocol === 'http:' && allowHttp) {
49
+ /* ok — WordPress legacy */
50
+ } else {
51
+ throw new Error('Seules les adresses http(s) publiques sont autorisées');
52
+ }
53
+ if (u.username || u.password) throw new Error('Identifiants dans l’URL interdits');
54
+ const host = u.hostname.toLowerCase();
55
+ if (!host || host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local') || host.endsWith('.internal')) {
56
+ throw new Error('Hôte local interdit');
57
+ }
58
+ if (net.isIP(host) && isPrivateIp(host)) throw new Error('Adresse IP privée interdite');
59
+ const port = u.port ? Number(u.port) : u.protocol === 'https:' ? 443 : 80;
60
+ if (BLOCKED_PORTS.has(port) || port < 1 || port > 65535) throw new Error('Port non autorisé');
61
+
62
+ let records: { address: string }[];
63
+ try {
64
+ records = await dns.lookup(host, { all: true, verbatim: true });
65
+ } catch {
66
+ throw new Error(`Impossible de résoudre ${host}`);
67
+ }
68
+ if (!records.length) throw new Error(`Hôte introuvable : ${host}`);
69
+ for (const r of records) {
70
+ if (isPrivateIp(r.address)) throw new Error('La destination résout vers un réseau privé (bloqué)');
71
+ }
72
+ return u;
73
+ }
74
+
75
+ /** Fetch avec redirections limitées, chaque hop re-validé (anti-SSRF). */
76
+ export async function outboundFetch(url: string, init?: RequestInit, maxRedirects = 3): Promise<Response> {
77
+ let current = await assertSafeOutboundUrl(url);
78
+ for (let i = 0; i <= maxRedirects; i++) {
79
+ const res = await fetch(current, {
80
+ ...init,
81
+ redirect: 'manual',
82
+ signal: init?.signal ?? AbortSignal.timeout(12_000),
83
+ });
84
+ if (res.status >= 300 && res.status < 400) {
85
+ const loc = res.headers.get('location');
86
+ if (!loc || i === maxRedirects) throw new Error('Trop de redirections');
87
+ current = await assertSafeOutboundUrl(new URL(loc, current));
88
+ continue;
89
+ }
90
+ return res;
91
+ }
92
+ throw new Error('Trop de redirections');
93
+ }
94
+
95
+ function assertFtpHost(host: string) {
96
+ const h = host.trim().toLowerCase();
97
+ if (!h || h === 'localhost' || h.endsWith('.local')) throw new Error('Hôte FTP interdit');
98
+ if (net.isIP(h) && isPrivateIp(h)) throw new Error('IP FTP privée interdite');
99
+ return h;
100
+ }
101
+
102
+ export async function ftpList(opts: {
103
+ host: string;
104
+ port?: number;
105
+ user: string;
106
+ password: string;
107
+ secure?: boolean;
108
+ path?: string;
109
+ }): Promise<string[]> {
110
+ const host = assertFtpHost(opts.host);
111
+ if (net.isIP(host)) {
112
+ /* already checked */
113
+ } else {
114
+ const records = await dns.lookup(host, { all: true });
115
+ for (const r of records) if (isPrivateIp(r.address)) throw new Error('FTP vers réseau privé bloqué');
116
+ }
117
+ const client = new Client(30_000);
118
+ try {
119
+ await client.access({
120
+ host,
121
+ port: opts.port || (opts.secure ? 990 : 21),
122
+ user: String(opts.user || '').slice(0, 120),
123
+ password: String(opts.password || '').slice(0, 200),
124
+ secure: opts.secure ? 'implicit' : false,
125
+ });
126
+ const list = await client.list(opts.path || '/');
127
+ return list.map((f) => f.name).slice(0, 500);
128
+ } finally {
129
+ client.close();
130
+ }
131
+ }
132
+
133
+ export async function ftpReadText(opts: {
134
+ host: string;
135
+ port?: number;
136
+ user: string;
137
+ password: string;
138
+ secure?: boolean;
139
+ path: string;
140
+ maxBytes?: number;
141
+ }): Promise<string> {
142
+ const host = assertFtpHost(opts.host);
143
+ if (!net.isIP(host)) {
144
+ const records = await dns.lookup(host, { all: true });
145
+ for (const r of records) if (isPrivateIp(r.address)) throw new Error('FTP vers réseau privé bloqué');
146
+ }
147
+ const remote = String(opts.path || '');
148
+ if (!remote.startsWith('/') || remote.includes('..')) throw new Error('Chemin FTP invalide');
149
+ const max = Math.min(Math.max(opts.maxBytes || 2_000_000, 1024), 5_000_000);
150
+ const client = new Client(30_000);
151
+ try {
152
+ await client.access({
153
+ host,
154
+ port: opts.port || (opts.secure ? 990 : 21),
155
+ user: String(opts.user || '').slice(0, 120),
156
+ password: String(opts.password || '').slice(0, 200),
157
+ secure: opts.secure ? 'implicit' : false,
158
+ });
159
+ const chunks: Buffer[] = [];
160
+ let size = 0;
161
+ const { Writable } = await import('stream');
162
+ const writable = new Writable({
163
+ write(chunk, _enc, cb) {
164
+ size += chunk.length;
165
+ if (size > max) {
166
+ cb(new Error('Fichier FTP trop volumineux'));
167
+ return;
168
+ }
169
+ chunks.push(Buffer.from(chunk));
170
+ cb();
171
+ },
172
+ });
173
+ await client.downloadTo(writable, remote);
174
+ return Buffer.concat(chunks).toString('utf8');
175
+ } finally {
176
+ client.close();
177
+ }
178
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Snapify — achat de modules officiels depuis le backoffice (Flouci + licence)
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ */
5
+ import crypto from 'crypto';
6
+ import fs from 'fs/promises';
7
+ import os from 'os';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import type { LicensePayload, SignedLicense } from '../../sdk/index';
11
+ import { encodeLicenseKey, sign } from '../../sdk/package';
12
+ import { HttpError } from '../http';
13
+ import { generatePayment } from '../platform/flouci';
14
+ import { TRUSTED_KEYS } from './trust';
15
+ import { getCatalog } from './marketplace';
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const FILE = path.resolve(__dirname, '../../data/module-purchases.json');
19
+
20
+ export type ModulePurchase = {
21
+ id: string;
22
+ moduleId: string;
23
+ moduleName: string;
24
+ amountMillimes: number;
25
+ status: 'pending' | 'paid' | 'failed';
26
+ paymentId?: string;
27
+ licenseKey?: string;
28
+ createdAt: string;
29
+ paidAt?: string;
30
+ buyerEmail?: string;
31
+ };
32
+
33
+ async function load(): Promise<ModulePurchase[]> {
34
+ try {
35
+ return JSON.parse(await fs.readFile(FILE, 'utf8')) as ModulePurchase[];
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ async function save(rows: ModulePurchase[]) {
42
+ await fs.mkdir(path.dirname(FILE), { recursive: true });
43
+ await fs.writeFile(FILE, JSON.stringify(rows, null, 2));
44
+ }
45
+
46
+ async function signingKey(): Promise<{ pem: string; kid: string }> {
47
+ const raw = process.env.SNAPIFY_SIGNING_KEY;
48
+ const kid = process.env.SNAPIFY_SIGNING_KID || 'novalabs-2026';
49
+ if (!raw) throw new HttpError(503, 'Signature impossible : SNAPIFY_SIGNING_KEY manquante.');
50
+ if (!TRUSTED_KEYS[kid]) throw new HttpError(503, `Clé « ${kid} » absente des clés de confiance.`);
51
+ const file = raw.startsWith('~/') ? path.join(os.homedir(), raw.slice(2)) : raw;
52
+ return { pem: await fs.readFile(file, 'utf8'), kid };
53
+ }
54
+
55
+ function issuePaidLicense(moduleId: string, licensee: string, key: { pem: string; kid: string }): SignedLicense {
56
+ const payload: LicensePayload = {
57
+ v: 1,
58
+ licenseId: `paid-${moduleId}-${crypto.randomUUID().slice(0, 8)}`,
59
+ moduleId,
60
+ licensee: licensee.slice(0, 120) || 'Acheteur Snapify',
61
+ plan: 'standard',
62
+ domains: ['*'],
63
+ issuedAt: new Date().toISOString(),
64
+ expiresAt: null,
65
+ };
66
+ return { payload, signature: sign(payload, key.pem, key.kid) };
67
+ }
68
+
69
+ /** Prix catalogue (millimes) — défaut 49 DT si payant sans prix. */
70
+ export function catalogPriceMillimes(entry: { pricing: string; priceMillimes?: number }): number {
71
+ if (entry.pricing !== 'paid') return 0;
72
+ const n = Number(entry.priceMillimes);
73
+ return Number.isFinite(n) && n > 0 ? Math.round(n) : 49_000;
74
+ }
75
+
76
+ export async function startModuleCheckout(input: {
77
+ moduleId: string;
78
+ publicBase: string;
79
+ buyerEmail?: string;
80
+ }) {
81
+ const catalog = await getCatalog();
82
+ const entry = catalog.modules.find((m) => m.id === input.moduleId && (m.kind ?? 'module') === 'module');
83
+ if (!entry) throw new HttpError(404, 'Module introuvable dans le catalogue');
84
+ if (entry.pricing !== 'paid') throw new HttpError(400, 'Ce module est gratuit — installez-le directement.');
85
+
86
+ const amountMillimes = catalogPriceMillimes(entry);
87
+ const id = crypto.randomUUID();
88
+ const trackingId = `module_${id}`;
89
+ const base = input.publicBase.replace(/\/+$/, '');
90
+ const successLink = `${base}/administrator/marketplace?purchase=ok&module=${encodeURIComponent(entry.id)}&pid=${id}`;
91
+ const failLink = `${base}/administrator/marketplace?purchase=fail&module=${encodeURIComponent(entry.id)}`;
92
+
93
+ const payment = await generatePayment({
94
+ amountMillimes,
95
+ trackingId,
96
+ successLink,
97
+ failLink,
98
+ webhook: `${base}/api/platform/flouci/webhook`,
99
+ });
100
+
101
+ const rows = await load();
102
+ const purchase: ModulePurchase = {
103
+ id,
104
+ moduleId: entry.id,
105
+ moduleName: entry.name,
106
+ amountMillimes,
107
+ status: 'pending',
108
+ paymentId: payment.paymentId,
109
+ createdAt: new Date().toISOString(),
110
+ buyerEmail: input.buyerEmail,
111
+ };
112
+ rows.unshift(purchase);
113
+ await save(rows.slice(0, 500));
114
+
115
+ return { purchase, paymentLink: payment.link, paymentId: payment.paymentId };
116
+ }
117
+
118
+ export async function markModulePurchasePaid(paymentId: string): Promise<ModulePurchase | null> {
119
+ const rows = await load();
120
+ let order =
121
+ rows.find((r) => r.paymentId === paymentId) ||
122
+ (paymentId.startsWith('mock_module_') ? rows.find((r) => r.id === paymentId.slice('mock_module_'.length)) : null) ||
123
+ null;
124
+ if (!order && paymentId.startsWith('mock_')) {
125
+ const tracking = paymentId.slice(5);
126
+ if (tracking.startsWith('module_')) order = rows.find((r) => r.id === tracking.slice('module_'.length)) || null;
127
+ }
128
+ if (!order) return null;
129
+ if (order.status === 'paid' && order.licenseKey) return order;
130
+
131
+ const key = await signingKey();
132
+ const license = issuePaidLicense(order.moduleId, order.buyerEmail || order.moduleName, key);
133
+ order.status = 'paid';
134
+ order.paidAt = new Date().toISOString();
135
+ order.licenseKey = encodeLicenseKey(license);
136
+ if (!order.paymentId) order.paymentId = paymentId;
137
+ await save(rows);
138
+ console.log(`[module-purchase] payé : ${order.moduleId} · ${order.id}`);
139
+ return order;
140
+ }
141
+
142
+ export async function getModulePurchase(id: string) {
143
+ return (await load()).find((r) => r.id === id) || null;
144
+ }
145
+
146
+ export async function latestPaidLicenseForModule(moduleId: string) {
147
+ const rows = await load();
148
+ return rows.find((r) => r.moduleId === moduleId && r.status === 'paid' && r.licenseKey) || null;
149
+ }
@@ -0,0 +1,168 @@
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
+ * Collections d'enregistrements des modules (`ctx.db.collection()`) : documents JSON dans
9
+ * `module_records`, isolés par module et par collection. Les filtres sont traduits en SQL
10
+ * paramétré ; seuls des noms de champs simples sont acceptés.
11
+ */
12
+ import crypto from 'crypto';
13
+ import { and, eq, inArray, sql, type SQL } from 'drizzle-orm';
14
+ import { db } from '../db/client';
15
+ import { moduleRecords } from '../db/schema';
16
+ import type { Collection, FindQuery, RecordFilter, StoredRecord } from '../../sdk/index';
17
+
18
+ const COLLECTION_PATTERN = /^[a-z][a-z0-9_-]{0,39}$/;
19
+ const FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,40}$/;
20
+ const ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
21
+ const MAX_RECORD_BYTES = 256 * 1024;
22
+ const MAX_RECORDS_PER_COLLECTION = 100_000;
23
+ const META = { id: moduleRecords.id, createdAt: moduleRecords.createdAt, updatedAt: moduleRecords.updatedAt } as const;
24
+
25
+ const ACCENTS_FROM = 'àáâãäåçèéêëìíîïñòóôõöùúûüýÿ';
26
+ const ACCENTS_TO = 'aaaaaaceeeeiiiinooooouuuuyy';
27
+ export const fold = (s: string) => s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
28
+
29
+ type Row = typeof moduleRecords.$inferSelect;
30
+
31
+ function toRecord<T>(row: Row): StoredRecord<T> {
32
+ return { ...(row.data as T), id: row.id, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString() };
33
+ }
34
+
35
+ function fieldExpr(field: string, numeric = false): SQL {
36
+ if (field in META) return sql`${META[field as keyof typeof META]}`;
37
+ if (!FIELD_PATTERN.test(field)) throw new Error(`champ invalide : ${field}`);
38
+ return numeric ? sql`(${moduleRecords.data}->>${field})::numeric` : sql`(${moduleRecords.data}->>${field})`;
39
+ }
40
+
41
+ function filterSql(f: RecordFilter): SQL {
42
+ const numeric = typeof f.value === 'number';
43
+ const col = fieldExpr(f.field, numeric);
44
+ const v = f.value;
45
+ switch (f.op) {
46
+ case 'eq':
47
+ return f.field in META ? sql`${col} = ${v}` : sql`${moduleRecords.data} @> ${JSON.stringify({ [f.field]: v })}::jsonb`;
48
+ case 'ne':
49
+ return sql`(${col} IS DISTINCT FROM ${numeric ? v : String(v)})`;
50
+ case 'gt':
51
+ return sql`${col} > ${v}`;
52
+ case 'gte':
53
+ return sql`${col} >= ${v}`;
54
+ case 'lt':
55
+ return sql`${col} < ${v}`;
56
+ case 'lte':
57
+ return sql`${col} <= ${v}`;
58
+ case 'in': {
59
+ const list = Array.isArray(v) ? v.slice(0, 500).map(String) : [];
60
+ if (!list.length) return sql`false`;
61
+ return sql`${fieldExpr(f.field)} IN (${sql.join(list.map((x) => sql`${x}`), sql`, `)})`;
62
+ }
63
+ case 'contains':
64
+ return sql`${moduleRecords.data} @> ${JSON.stringify({ [f.field]: [v] })}::jsonb`;
65
+ case 'exists':
66
+ return v === false ? sql`NOT (${moduleRecords.data} ? ${f.field})` : sql`${moduleRecords.data} ? ${f.field}`;
67
+ default:
68
+ throw new Error(`opérateur inconnu : ${String(f.op)}`);
69
+ }
70
+ }
71
+
72
+ function whereSql(moduleId: string, collection: string, q: Pick<FindQuery, 'where' | 'filters' | 'search'> = {}): SQL {
73
+ const parts: SQL[] = [eq(moduleRecords.moduleId, moduleId), eq(moduleRecords.collection, collection)];
74
+ if (q.where && Object.keys(q.where).length) {
75
+ for (const k of Object.keys(q.where)) if (!FIELD_PATTERN.test(k)) throw new Error(`champ invalide : ${k}`);
76
+ parts.push(sql`${moduleRecords.data} @> ${JSON.stringify(q.where)}::jsonb`);
77
+ }
78
+ for (const f of q.filters ?? []) parts.push(filterSql(f));
79
+ const text = q.search?.text ? fold(q.search.text.trim()).slice(0, 100) : '';
80
+ if (text && q.search?.fields.length) {
81
+ const like = `%${text.replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
82
+ const ors = q.search.fields.slice(0, 10).map((f) => sql`translate(lower(coalesce(${fieldExpr(f)}, '')), ${ACCENTS_FROM}, ${ACCENTS_TO}) LIKE ${like}`);
83
+ parts.push(sql`(${sql.join(ors, sql` OR `)})`);
84
+ }
85
+ return and(...parts)!;
86
+ }
87
+
88
+ function serialize(data: unknown): Record<string, unknown> {
89
+ if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error('un enregistrement doit être un objet');
90
+ const { id: _id, createdAt: _c, updatedAt: _u, ...rest } = data as Record<string, unknown>;
91
+ const json = JSON.stringify(rest);
92
+ if (Buffer.byteLength(json) > MAX_RECORD_BYTES) throw new Error('enregistrement trop volumineux (256 Ko max.)');
93
+ return JSON.parse(json);
94
+ }
95
+
96
+ export function createCollection<T>(moduleId: string, name: string): Collection<T> {
97
+ if (!COLLECTION_PATTERN.test(name)) throw new Error(`[module:${moduleId}] nom de collection invalide : ${name}`);
98
+ const scope = and(eq(moduleRecords.moduleId, moduleId), eq(moduleRecords.collection, name))!;
99
+ const byId = (id: string) => and(scope, eq(moduleRecords.id, id))!;
100
+ const validId = (id: unknown): id is string => typeof id === 'string' && ID_PATTERN.test(id);
101
+
102
+ const find: Collection<T>['find'] = async (q = {}) => {
103
+ const where = whereSql(moduleId, name, q);
104
+ const limit = Math.min(500, Math.max(1, Math.floor(q.limit ?? 100)));
105
+ const offset = Math.max(0, Math.floor(q.offset ?? 0));
106
+ const order = (q.orderBy?.length ? q.orderBy : [{ field: 'createdAt', dir: 'desc' as const }])
107
+ .slice(0, 3)
108
+ .map((o) => (o.dir === 'asc' ? sql`${fieldExpr(o.field, o.numeric)} ASC NULLS LAST` : sql`${fieldExpr(o.field, o.numeric)} DESC NULLS LAST`));
109
+ const [rows, [{ total }]] = await Promise.all([
110
+ db.select().from(moduleRecords).where(where).orderBy(...order, sql`${moduleRecords.id}`).limit(limit).offset(offset),
111
+ db.select({ total: sql<number>`count(*)::int` }).from(moduleRecords).where(where),
112
+ ]);
113
+ return { items: rows.map((r) => toRecord<T>(r)), total };
114
+ };
115
+
116
+ return {
117
+ async get(id) {
118
+ if (!validId(id)) return null;
119
+ const [row] = await db.select().from(moduleRecords).where(byId(id));
120
+ return row ? toRecord<T>(row) : null;
121
+ },
122
+ async getMany(ids) {
123
+ const list = [...new Set(ids.filter(validId))].slice(0, 500);
124
+ if (!list.length) return [];
125
+ const rows = await db.select().from(moduleRecords).where(and(scope, inArray(moduleRecords.id, list)));
126
+ const map = new Map(rows.map((r) => [r.id, toRecord<T>(r)]));
127
+ return list.map((id) => map.get(id)).filter((r): r is StoredRecord<T> => !!r);
128
+ },
129
+ find,
130
+ async findOne(where) {
131
+ const { items } = await find({ where, limit: 1 });
132
+ return items[0] ?? null;
133
+ },
134
+ async count(q = {}) {
135
+ const [{ total }] = await db.select({ total: sql<number>`count(*)::int` }).from(moduleRecords).where(whereSql(moduleId, name, q));
136
+ return total;
137
+ },
138
+ async insert(data, opts = {}) {
139
+ const id = opts.id ?? crypto.randomBytes(9).toString('base64url');
140
+ if (!validId(id)) throw new Error('identifiant d’enregistrement invalide');
141
+ const [{ n }] = await db.select({ n: sql<number>`count(*)::int` }).from(moduleRecords).where(scope);
142
+ if (n >= MAX_RECORDS_PER_COLLECTION) throw new Error(`collection ${name} pleine (${MAX_RECORDS_PER_COLLECTION} enregistrements)`);
143
+ const now = new Date();
144
+ const [row] = await db
145
+ .insert(moduleRecords)
146
+ .values({ moduleId, collection: name, id, data: serialize(data), createdAt: now, updatedAt: now })
147
+ .returning();
148
+ return toRecord<T>(row);
149
+ },
150
+ async update(id, patch) {
151
+ if (!validId(id)) return null;
152
+ const [current] = await db.select().from(moduleRecords).where(byId(id));
153
+ if (!current) return null;
154
+ const data = serialize({ ...(current.data as object), ...(patch as object) });
155
+ const [row] = await db.update(moduleRecords).set({ data, updatedAt: new Date() }).where(byId(id)).returning();
156
+ return row ? toRecord<T>(row) : null;
157
+ },
158
+ async delete(id) {
159
+ if (!validId(id)) return false;
160
+ const rows = await db.delete(moduleRecords).where(byId(id)).returning({ id: moduleRecords.id });
161
+ return rows.length > 0;
162
+ },
163
+ };
164
+ }
165
+
166
+ export async function deleteModuleRecords(moduleId: string) {
167
+ await db.delete(moduleRecords).where(eq(moduleRecords.moduleId, moduleId));
168
+ }