create-nextblock 0.12.9 → 0.12.11

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 (292) hide show
  1. package/docker-template/.dockerignore +23 -23
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/libs/db/tsconfig.lib.json +3 -3
  8. package/libs/editor/tsconfig.lib.json +3 -3
  9. package/libs/ui/tsconfig.lib.json +3 -3
  10. package/libs/utils/tsconfig.json +3 -3
  11. package/package.json +1 -1
  12. package/project.json +19 -19
  13. package/templates/nextblock-template/.browserslistrc +11 -11
  14. package/templates/nextblock-template/.dockerignore +23 -23
  15. package/templates/nextblock-template/README.md +34 -34
  16. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  17. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  18. package/templates/nextblock-template/app/(auth-pages)/sign-up/SignUpForm.tsx +141 -141
  19. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  20. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -91
  21. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +123 -123
  22. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  23. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
  24. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  25. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  26. package/templates/nextblock-template/app/actions/interactions.test.ts +301 -301
  27. package/templates/nextblock-template/app/actions/interactions.ts +373 -373
  28. package/templates/nextblock-template/app/actions/postActions.ts +146 -146
  29. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  30. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  31. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  32. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +964 -964
  33. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  34. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  35. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  36. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  37. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
  38. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5597 -5597
  39. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  40. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  41. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  42. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  46. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
  47. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  48. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  49. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  50. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  51. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  52. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  53. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
  54. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  55. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  56. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -244
  57. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  58. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  59. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  60. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  61. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  62. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  63. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  64. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -1522
  65. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  66. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  67. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  68. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  69. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  70. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  71. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  72. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +431 -391
  73. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  74. package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -408
  75. package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -51
  76. package/templates/nextblock-template/app/cms/layout.tsx +73 -73
  77. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  78. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  79. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  80. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  81. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +149 -149
  82. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -101
  83. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  84. package/templates/nextblock-template/app/cms/pages/page.tsx +18 -17
  85. package/templates/nextblock-template/app/cms/posts/page.tsx +19 -18
  86. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  87. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  88. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  89. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  90. package/templates/nextblock-template/app/cms/products/page.tsx +39 -27
  91. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  92. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  93. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  94. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  95. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
  98. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  99. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  100. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -93
  101. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -129
  102. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  103. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +497 -497
  104. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -410
  105. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +246 -246
  106. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -80
  107. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  108. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  109. package/templates/nextblock-template/app/cms/settings/email/actions.ts +64 -64
  110. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +181 -181
  111. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  112. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  113. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  114. package/templates/nextblock-template/app/cms/settings/google-analytics/actions.ts +60 -60
  115. package/templates/nextblock-template/app/cms/settings/google-analytics/components/GoogleAnalyticsForm.tsx +129 -129
  116. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  117. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +16 -16
  118. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  119. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  120. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  121. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -20
  122. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  123. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  124. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  125. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
  126. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +52 -52
  127. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +148 -148
  128. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  129. package/templates/nextblock-template/app/cms/settings/registration/actions.ts +44 -44
  130. package/templates/nextblock-template/app/cms/settings/registration/components/RegistrationForm.tsx +65 -65
  131. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  132. package/templates/nextblock-template/app/cms/settings/security/actions.ts +284 -284
  133. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +522 -522
  134. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  135. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  136. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  137. package/templates/nextblock-template/app/cms/users/actions.ts +222 -222
  138. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  139. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +44 -44
  140. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  141. package/templates/nextblock-template/app/lib/sitemap-utils.ts +253 -253
  142. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -73
  143. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  144. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  145. package/templates/nextblock-template/app/profile/account-data.ts +37 -37
  146. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  147. package/templates/nextblock-template/app/profile/account-types.ts +11 -11
  148. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  149. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -79
  150. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -32
  151. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  152. package/templates/nextblock-template/app/profile/page.tsx +42 -42
  153. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  154. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  155. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  156. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  157. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  158. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  159. package/templates/nextblock-template/app/sitemap.ts +130 -130
  160. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  161. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  162. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  163. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  164. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  165. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  166. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  167. package/templates/nextblock-template/components/GitHubLoginButton.tsx +15 -15
  168. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  169. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  170. package/templates/nextblock-template/components/PostCommentsSection.tsx +369 -369
  171. package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -419
  172. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  173. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  174. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  175. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  176. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  177. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  178. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
  179. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  180. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  181. package/templates/nextblock-template/components/form-message.tsx +32 -32
  182. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  183. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  184. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  185. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  186. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -464
  187. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  188. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  189. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  190. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  191. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  192. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  193. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  194. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  195. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  196. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  197. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  198. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
  199. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
  200. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
  201. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  202. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -1319
  203. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  204. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
  205. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
  206. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
  207. package/templates/nextblock-template/docs/README.md +39 -39
  208. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12505 -12505
  209. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  210. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  211. package/templates/nextblock-template/index.d.ts +7 -7
  212. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  213. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  214. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  215. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  216. package/templates/nextblock-template/lib/auth/twoFactor.ts +208 -208
  217. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  218. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  219. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +6 -6
  220. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -18
  221. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  222. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +100 -77
  223. package/templates/nextblock-template/lib/cms-transfer/csv.ts +403 -399
  224. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
  225. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  226. package/templates/nextblock-template/lib/config/email-settings.ts +217 -217
  227. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  228. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  229. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  230. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  231. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -256
  232. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  233. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  234. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  235. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  236. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  237. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  238. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  239. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  240. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  241. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  242. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  243. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  244. package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
  245. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  246. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
  247. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  248. package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
  249. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  250. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  251. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  252. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  253. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  254. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  255. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  256. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  257. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  258. package/templates/nextblock-template/lib/search/server.ts +585 -585
  259. package/templates/nextblock-template/lib/search/types.ts +27 -27
  260. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  261. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  262. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  263. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -37
  264. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  265. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  266. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  267. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  268. package/templates/nextblock-template/lib/site-url.ts +48 -48
  269. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  270. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  271. package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
  272. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  273. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  274. package/templates/nextblock-template/package.json +1 -1
  275. package/templates/nextblock-template/postcss.config.js +6 -6
  276. package/templates/nextblock-template/scripts/backup.js +115 -115
  277. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  278. package/templates/nextblock-template/scripts/restore.js +385 -385
  279. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  280. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  281. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  282. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  283. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  284. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  285. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  286. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  287. package/templates/nextblock-template/tailwind.config.js +25 -25
  288. package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
  289. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  290. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
  291. package/tsconfig.base.json +3 -3
  292. package/templates/nextblock-template/tsconfig.tsbuildinfo +0 -1
@@ -1,460 +1,460 @@
1
- 'use server';
2
- // Server actions backing the browser /setup wizard. Every mutating action is guarded
3
- // by assertNotProvisioned() so setup can only run once (until a first admin exists).
4
-
5
- import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
6
- import { createClient as createSupabaseJsClient } from '@supabase/supabase-js';
7
- import { isLocalWritableEnv } from './env-status';
8
- import { writeEnvLocal } from './env-write';
9
- import {
10
- assertNotProvisioned,
11
- getProvisioningStatus,
12
- type ProvisioningStatus,
13
- } from './provisioning';
14
- import { applyMigrations, resetDatabase } from './schema-apply';
15
- import { setSystemConfigurationServiceRole } from './system-config';
16
- import { getStorageBackend } from '../storage/provider';
17
- import { ensureStorageBucket } from '../storage/supabase-storage';
18
-
19
- export interface ActionResult {
20
- ok: boolean;
21
- error?: string;
22
- message?: string;
23
- restartRecommended?: boolean;
24
- schemaReady?: boolean;
25
- }
26
-
27
- export interface ConnectionInput {
28
- supabaseUrl: string;
29
- anonKey: string;
30
- serviceRoleKey: string;
31
- postgresUrl?: string;
32
- siteUrl?: string;
33
- /** Supabase personal access token — needed by `npm run db:migrate` to link + push. */
34
- accessToken?: string;
35
- }
36
-
37
- /**
38
- * Sanity-check a Supabase API key offline. Legacy keys are JWTs carrying { role, ref };
39
- * newer keys are opaque sb_secret_* / sb_publishable_* strings. We use this to reject an
40
- * anon key pasted into the service-role field (and vice-versa) BEFORE it gets written —
41
- * otherwise it only surfaces much later as "permission denied" on the first write, since
42
- * a SELECT probe can't tell the keys apart (anon can also read site_settings).
43
- */
44
- function inspectSupabaseKey(key: string): {
45
- role?: string;
46
- ref?: string;
47
- format: 'jwt' | 'secret' | 'publishable' | 'unknown';
48
- } {
49
- if (key.startsWith('sb_secret_')) return { role: 'service_role', format: 'secret' };
50
- if (key.startsWith('sb_publishable_')) return { role: 'anon', format: 'publishable' };
51
- const parts = key.split('.');
52
- if (parts.length === 3) {
53
- try {
54
- const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
55
- return {
56
- role: typeof payload?.role === 'string' ? payload.role : undefined,
57
- ref: typeof payload?.ref === 'string' ? payload.ref : undefined,
58
- format: 'jwt',
59
- };
60
- } catch {
61
- // not a decodable JWT — fall through to 'unknown'
62
- }
63
- }
64
- return { format: 'unknown' };
65
- }
66
-
67
- /**
68
- * Step (Profile B / local only): validate the Supabase credentials, then persist them
69
- * to `.env.local` and the live process. Probes with the service-role key so we can
70
- * also report whether the schema has been applied yet.
71
- */
72
- export async function saveSupabaseConnection(input: ConnectionInput): Promise<ActionResult> {
73
- await assertNotProvisioned();
74
-
75
- const supabaseUrl = input.supabaseUrl?.trim();
76
- const anonKey = input.anonKey?.trim();
77
- const serviceRoleKey = input.serviceRoleKey?.trim();
78
-
79
- if (!supabaseUrl || !anonKey || !serviceRoleKey) {
80
- return {
81
- ok: false,
82
- error: 'Supabase URL, anon key, and service-role key are all required.',
83
- };
84
- }
85
- try {
86
- new URL(supabaseUrl);
87
- } catch {
88
- return { ok: false, error: 'The Supabase URL is not a valid URL.' };
89
- }
90
-
91
- // The anon and service-role keys are easy to swap (both start with "eyJ"). Catch a
92
- // swapped or wrong-project key here, offline, with a clear message.
93
- const svcKey = inspectSupabaseKey(serviceRoleKey);
94
- if (svcKey.role && svcKey.role !== 'service_role') {
95
- return {
96
- ok: false,
97
- error: `That service-role key is actually the "${svcKey.role}" key. Paste the secret service_role key from Supabase → Project Settings → API.`,
98
- };
99
- }
100
- const anonKeyInfo = inspectSupabaseKey(anonKey);
101
- if (anonKeyInfo.role && anonKeyInfo.role !== 'anon') {
102
- return {
103
- ok: false,
104
- error: `That anon key is actually the "${anonKeyInfo.role}" key. Paste the public anon key from Supabase → Project Settings → API.`,
105
- };
106
- }
107
- let urlRef: string | undefined;
108
- try {
109
- const host = new URL(supabaseUrl).hostname;
110
- if (host.endsWith('.supabase.co') || host.endsWith('.supabase.in')) {
111
- urlRef = host.split('.')[0];
112
- }
113
- } catch {
114
- // already validated above
115
- }
116
- if (urlRef && svcKey.ref && svcKey.ref !== urlRef) {
117
- return {
118
- ok: false,
119
- error: `That service-role key belongs to project "${svcKey.ref}", but the URL is project "${urlRef}". Use keys from the same project.`,
120
- };
121
- }
122
-
123
- if (!isLocalWritableEnv()) {
124
- return {
125
- ok: false,
126
- error:
127
- 'This environment is read-only. Set the Supabase variables on your hosting platform instead of here.',
128
- };
129
- }
130
-
131
- // Validate the credentials with a service-role probe before writing anything.
132
- const probe = createSupabaseJsClient(supabaseUrl, serviceRoleKey, {
133
- auth: { persistSession: false, autoRefreshToken: false },
134
- });
135
-
136
- // Definitive service-role check: only the service_role can call the GoTrue admin API.
137
- // A SELECT on site_settings can't tell service_role from anon (both can read it), so
138
- // this catches a rotated/invalid key that the offline inspection above can't. Works on
139
- // a fresh project too (the auth schema always exists, independent of the public schema).
140
- try {
141
- const { error: adminErr } = await probe.auth.admin.listUsers({ page: 1, perPage: 1 });
142
- if (adminErr) {
143
- return {
144
- ok: false,
145
- error: `That key can't perform admin actions (${adminErr.message}). Confirm you pasted the secret service_role key from Supabase → Project Settings → API.`,
146
- };
147
- }
148
- } catch (caught) {
149
- return {
150
- ok: false,
151
- error: `Could not verify the service-role key: ${
152
- caught instanceof Error ? caught.message : 'unknown error'
153
- }`,
154
- };
155
- }
156
-
157
- let schemaReady = false;
158
- try {
159
- const { error } = await probe.from('site_settings').select('key').limit(1);
160
- if (error) {
161
- const missing = /relation|does not exist|schema cache/i.test(error.message);
162
- if (!missing) {
163
- return {
164
- ok: false,
165
- error: `Could not reach Supabase with those credentials: ${error.message}`,
166
- };
167
- }
168
- schemaReady = false; // reachable, but the schema isn't applied yet
169
- } else {
170
- schemaReady = true;
171
- }
172
- } catch (caught) {
173
- return {
174
- ok: false,
175
- error: `Could not connect to Supabase: ${
176
- caught instanceof Error ? caught.message : 'unknown error'
177
- }`,
178
- };
179
- }
180
-
181
- const values: Record<string, string> = {
182
- NEXT_PUBLIC_SUPABASE_URL: supabaseUrl,
183
- NEXT_PUBLIC_SUPABASE_ANON_KEY: anonKey,
184
- SUPABASE_SERVICE_ROLE_KEY: serviceRoleKey,
185
- // Enable the build-time migration hook for this install (Milestone 4). On Vercel
186
- // VERCEL_ENV gates it instead; off-Vercel (local / Docker / standalone) production
187
- // builds read this flag. Only written to a local-writable .env (no-op on Vercel).
188
- NEXTBLOCK_BUILD_MIGRATE: '1',
189
- };
190
- if (input.siteUrl?.trim()) values.NEXT_PUBLIC_URL = input.siteUrl.trim();
191
- if (input.postgresUrl?.trim()) values.POSTGRES_URL = input.postgresUrl.trim();
192
- if (input.accessToken?.trim()) values.SUPABASE_ACCESS_TOKEN = input.accessToken.trim();
193
-
194
- // Derive the project ref from the URL (https://<ref>.supabase.co) so the CLI schema
195
- // step (`npm run db:migrate`) links + pushes to THIS project, not a stale one.
196
- try {
197
- const host = new URL(supabaseUrl).hostname;
198
- if (host.endsWith('.supabase.co') || host.endsWith('.supabase.in')) {
199
- values.SUPABASE_PROJECT_ID = host.split('.')[0];
200
- }
201
- } catch {
202
- // already validated above; ignore
203
- }
204
-
205
- try {
206
- await writeEnvLocal(values);
207
- } catch (caught) {
208
- return {
209
- ok: false,
210
- error: `Could not write .env.local: ${
211
- caught instanceof Error ? caught.message : 'unknown error'
212
- }`,
213
- };
214
- }
215
-
216
- return {
217
- ok: true,
218
- schemaReady,
219
- restartRecommended: true,
220
- message: schemaReady
221
- ? 'Connection saved and verified.'
222
- : 'Connection saved. The database schema is not applied yet — run "npm run db:migrate", then re-check below.',
223
- };
224
- }
225
-
226
- /** Poll provisioning status (used by the wizard to advance past connection/schema). */
227
- export async function recheckStatus(): Promise<ProvisioningStatus & { writable: boolean }> {
228
- const status = await getProvisioningStatus();
229
- return { ...status, writable: isLocalWritableEnv() };
230
- }
231
-
232
- export interface CompleteSetupInput {
233
- admin: { email: string; password: string; fullName: string };
234
- /** Local-only extra env (media storage) collected by the wizard. */
235
- envValues?: Record<string, string>;
236
- /** "Start from a clean database" — wipe before installing (local dev only, server-gated). */
237
- resetFirst?: boolean;
238
- }
239
-
240
- /**
241
- * After a fresh migration, PostgREST may briefly not see the new tables (its schema
242
- * cache). applyMigrations issues a reload, but it's async — poll until a known new
243
- * table reads cleanly so the REST reads/writes below don't hit a false "table not
244
- * found". Best-effort: gives up after ~6s and lets the caller proceed.
245
- */
246
- async function waitForSchemaCache(
247
- supabase: ReturnType<typeof getServiceRoleSupabaseClient>,
248
- ): Promise<void> {
249
- for (let attempt = 0; attempt < 12; attempt++) {
250
- const { error } = await supabase.from('system_configuration').select('id').limit(1);
251
- if (!error) return;
252
- await new Promise((resolve) => setTimeout(resolve, 500));
253
- }
254
- }
255
-
256
- /**
257
- * Final step: persist remaining settings and create the first admin. The
258
- * handle_new_user trigger assigns the ADMIN role and flips is_admin_created, so we
259
- * never set the role ourselves. `email_confirm: true` means no SMTP round-trip is
260
- * required, which keeps every channel (including cloud-without-SMTP) unblocked.
261
- */
262
- export async function completeSetup(input: CompleteSetupInput): Promise<ActionResult> {
263
- // "Start from a clean database" (local dev only) deliberately re-installs over an
264
- // existing DB — it wipes everything first — so the one-shot guard is skipped in that
265
- // case. Otherwise enforce it: a provisioned instance can't be re-setup or wiped, and
266
- // we return a clean message rather than throwing an unhandled error.
267
- const willReset = input.resetFirst === true && isLocalWritableEnv();
268
- if (!willReset) {
269
- try {
270
- await assertNotProvisioned();
271
- } catch {
272
- return {
273
- ok: false,
274
- error:
275
- 'Setup has already been completed. Enable "Start from a clean database" to reinstall, or sign in instead.',
276
- };
277
- }
278
- }
279
-
280
- const email = input.admin?.email?.trim().toLowerCase();
281
- const password = input.admin?.password ?? '';
282
- const fullName = input.admin?.fullName?.trim() ?? '';
283
-
284
- if (!email || !password) {
285
- return { ok: false, error: 'Administrator email and password are required.' };
286
- }
287
- if (password.length < 8) {
288
- return { ok: false, error: 'Use an administrator password of at least 8 characters.' };
289
- }
290
-
291
- // 1) Persist any local env extras (media storage) for Profile B.
292
- if (
293
- input.envValues &&
294
- Object.keys(input.envValues).length > 0 &&
295
- isLocalWritableEnv()
296
- ) {
297
- try {
298
- await writeEnvLocal(input.envValues);
299
- } catch (caught) {
300
- return {
301
- ok: false,
302
- error: `Could not write .env.local: ${
303
- caught instanceof Error ? caught.message : 'unknown error'
304
- }`,
305
- };
306
- }
307
- }
308
-
309
- // 2) Service-role access is required from here on.
310
- let admin: ReturnType<typeof getServiceRoleSupabaseClient>;
311
- try {
312
- admin = getServiceRoleSupabaseClient();
313
- } catch {
314
- return {
315
- ok: false,
316
- error:
317
- 'The service-role key is not loaded yet. Restart the dev server after saving the connection, then retry.',
318
- };
319
- }
320
-
321
- // 2.5) "Start from a clean database" (the fresh-install checkbox): wipe the DB (public
322
- // schema + migration history + auth users) before installing, so each fresh setup
323
- // starts clean. Two reliable guards: assertNotProvisioned() above means this only
324
- // runs when no admin exists (a live site is immune), and isLocalWritableEnv()
325
- // restricts it to local dev (a deployed app can never be tricked into wiping
326
- // itself, even by a crafted request).
327
- if (willReset) {
328
- const reset = await resetDatabase();
329
- if (!reset.ok) {
330
- return { ok: false, error: `Could not reset the database: ${reset.error}` };
331
- }
332
- // Clear auth users via the admin API (reliable — SQL on the auth schema can be
333
- // permission-restricted), so the admin email is free to reuse on this fresh install.
334
- try {
335
- const { data: list } = await admin.auth.admin.listUsers();
336
- for (const existing of list?.users ?? []) {
337
- await admin.auth.admin.deleteUser(existing.id);
338
- }
339
- } catch {
340
- // Non-fatal — the createUser "already exists" fallback below also handles leftovers.
341
- }
342
- }
343
-
344
- // 3) Apply the database schema if it isn't there yet (e.g. a fresh Supabase project).
345
- // Direct Postgres connection — no CLI, no config.toml. Idempotent (tracks applied
346
- // migrations), so it's a no-op when the schema already exists (Docker, or a re-run).
347
- // Then wait for PostgREST to pick up the new tables before we touch them via REST.
348
- const schemaStatus = await getProvisioningStatus();
349
- if (!schemaStatus.schemaReady) {
350
- // applyMigrations prefers the Supabase Management API (HTTPS, needs only the access
351
- // token + project ref) and falls back to a direct Postgres connection. It returns a
352
- // clear error if neither is available.
353
- const schema = await applyMigrations();
354
- if (!schema.ok) {
355
- return { ok: false, error: `Could not apply the database schema: ${schema.error}` };
356
- }
357
- }
358
-
359
- // Ensure PostgREST has the schema cached before ANY REST read/write below — covers a
360
- // fresh apply AND a cold cache over a pre-existing schema (Docker boot race / re-run).
361
- await waitForSchemaCache(admin);
362
-
363
- // On the native Supabase Storage backend (zero-key Vercel deploy), provision the public
364
- // media bucket now so the first upload doesn't have to. Best-effort — the upload path
365
- // also ensures it lazily, so a transient failure here never blocks finishing setup.
366
- if (getStorageBackend() === 'supabase') {
367
- try {
368
- await ensureStorageBucket();
369
- } catch (caught) {
370
- console.warn(
371
- 'Could not pre-create the Supabase Storage media bucket (will retry on first upload):',
372
- caught instanceof Error ? caught.message : caught,
373
- );
374
- }
375
- }
376
-
377
- // 4) Persist DB-backed settings (service role bypasses RLS — no admin exists yet).
378
- // Sign-up policy, bot protection, email, and payments are no longer collected by the
379
- // wizard; they are configured later from the CMS. New sign-ups default to requiring
380
- // email verification (auto_accept_signups = false) as a safe default.
381
- try {
382
- await setSystemConfigurationServiceRole({ auto_accept_signups: false });
383
- } catch (caught) {
384
- return {
385
- ok: false,
386
- error: `Failed to save settings: ${
387
- caught instanceof Error ? caught.message : 'unknown error'
388
- }`,
389
- };
390
- }
391
-
392
- // 4) Create the first admin account (already confirmed). The handle_new_user trigger
393
- // assigns ADMIN to the very first user and flips is_admin_created atomically.
394
- const createPayload = {
395
- email,
396
- password,
397
- email_confirm: true,
398
- user_metadata: { full_name: fullName },
399
- };
400
- let { data: created, error: createError } = await admin.auth.admin.createUser(createPayload);
401
-
402
- // If a clean-install reset left a stale auth user (admin-API cleanup above hit an edge
403
- // case), remove it and retry once so the operator can reuse their email.
404
- if (
405
- createError &&
406
- input.resetFirst === true &&
407
- /already|registered|exists/i.test(createError.message)
408
- ) {
409
- try {
410
- const { data: list } = await admin.auth.admin.listUsers();
411
- const stale = list?.users?.find((u) => u.email?.toLowerCase() === email);
412
- if (stale) {
413
- await admin.auth.admin.deleteUser(stale.id);
414
- ({ data: created, error: createError } = await admin.auth.admin.createUser(createPayload));
415
- }
416
- } catch {
417
- // fall through to the error handling below
418
- }
419
- }
420
-
421
- if (createError || !created?.user) {
422
- if (createError && /already|registered|exists/i.test(createError.message)) {
423
- return {
424
- ok: false,
425
- error:
426
- 'An account with this email already exists. Enable "Start from a clean database", or use a different email.',
427
- };
428
- }
429
- return {
430
- ok: false,
431
- error: `Could not create the administrator account: ${
432
- createError?.message ?? 'unknown error'
433
- }`,
434
- };
435
- }
436
-
437
- // Guard against a concurrent-setup race: assertNotProvisioned() is a check-then-act,
438
- // so two simultaneous submissions could both pass it. The trigger still grants ADMIN
439
- // to only the first user — so if this account came back as anything other than ADMIN,
440
- // another session won the race. Undo this spurious account and report it, rather than
441
- // signing the operator in as a non-admin.
442
- const { data: createdProfile } = await admin
443
- .from('profiles')
444
- .select('role')
445
- .eq('id', created.user.id)
446
- .maybeSingle();
447
- if (createdProfile?.role !== 'ADMIN') {
448
- await admin.auth.admin.deleteUser(created.user.id).catch((err) => {
449
- console.warn('Failed to clean up stale admin user after race condition:', err);
450
- });
451
- return {
452
- ok: false,
453
- error: 'Setup was just completed by another session. Please sign in instead.',
454
- };
455
- }
456
-
457
- // The wizard establishes the session afterwards via the canonical signInAction (a more
458
- // reliable cookie path than signing in here), so we just report success.
459
- return { ok: true, message: 'Setup complete.' };
460
- }
1
+ 'use server';
2
+ // Server actions backing the browser /setup wizard. Every mutating action is guarded
3
+ // by assertNotProvisioned() so setup can only run once (until a first admin exists).
4
+
5
+ import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
6
+ import { createClient as createSupabaseJsClient } from '@supabase/supabase-js';
7
+ import { isLocalWritableEnv } from './env-status';
8
+ import { writeEnvLocal } from './env-write';
9
+ import {
10
+ assertNotProvisioned,
11
+ getProvisioningStatus,
12
+ type ProvisioningStatus,
13
+ } from './provisioning';
14
+ import { applyMigrations, resetDatabase } from './schema-apply';
15
+ import { setSystemConfigurationServiceRole } from './system-config';
16
+ import { getStorageBackend } from '../storage/provider';
17
+ import { ensureStorageBucket } from '../storage/supabase-storage';
18
+
19
+ export interface ActionResult {
20
+ ok: boolean;
21
+ error?: string;
22
+ message?: string;
23
+ restartRecommended?: boolean;
24
+ schemaReady?: boolean;
25
+ }
26
+
27
+ export interface ConnectionInput {
28
+ supabaseUrl: string;
29
+ anonKey: string;
30
+ serviceRoleKey: string;
31
+ postgresUrl?: string;
32
+ siteUrl?: string;
33
+ /** Supabase personal access token — needed by `npm run db:migrate` to link + push. */
34
+ accessToken?: string;
35
+ }
36
+
37
+ /**
38
+ * Sanity-check a Supabase API key offline. Legacy keys are JWTs carrying { role, ref };
39
+ * newer keys are opaque sb_secret_* / sb_publishable_* strings. We use this to reject an
40
+ * anon key pasted into the service-role field (and vice-versa) BEFORE it gets written —
41
+ * otherwise it only surfaces much later as "permission denied" on the first write, since
42
+ * a SELECT probe can't tell the keys apart (anon can also read site_settings).
43
+ */
44
+ function inspectSupabaseKey(key: string): {
45
+ role?: string;
46
+ ref?: string;
47
+ format: 'jwt' | 'secret' | 'publishable' | 'unknown';
48
+ } {
49
+ if (key.startsWith('sb_secret_')) return { role: 'service_role', format: 'secret' };
50
+ if (key.startsWith('sb_publishable_')) return { role: 'anon', format: 'publishable' };
51
+ const parts = key.split('.');
52
+ if (parts.length === 3) {
53
+ try {
54
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
55
+ return {
56
+ role: typeof payload?.role === 'string' ? payload.role : undefined,
57
+ ref: typeof payload?.ref === 'string' ? payload.ref : undefined,
58
+ format: 'jwt',
59
+ };
60
+ } catch {
61
+ // not a decodable JWT — fall through to 'unknown'
62
+ }
63
+ }
64
+ return { format: 'unknown' };
65
+ }
66
+
67
+ /**
68
+ * Step (Profile B / local only): validate the Supabase credentials, then persist them
69
+ * to `.env.local` and the live process. Probes with the service-role key so we can
70
+ * also report whether the schema has been applied yet.
71
+ */
72
+ export async function saveSupabaseConnection(input: ConnectionInput): Promise<ActionResult> {
73
+ await assertNotProvisioned();
74
+
75
+ const supabaseUrl = input.supabaseUrl?.trim();
76
+ const anonKey = input.anonKey?.trim();
77
+ const serviceRoleKey = input.serviceRoleKey?.trim();
78
+
79
+ if (!supabaseUrl || !anonKey || !serviceRoleKey) {
80
+ return {
81
+ ok: false,
82
+ error: 'Supabase URL, anon key, and service-role key are all required.',
83
+ };
84
+ }
85
+ try {
86
+ new URL(supabaseUrl);
87
+ } catch {
88
+ return { ok: false, error: 'The Supabase URL is not a valid URL.' };
89
+ }
90
+
91
+ // The anon and service-role keys are easy to swap (both start with "eyJ"). Catch a
92
+ // swapped or wrong-project key here, offline, with a clear message.
93
+ const svcKey = inspectSupabaseKey(serviceRoleKey);
94
+ if (svcKey.role && svcKey.role !== 'service_role') {
95
+ return {
96
+ ok: false,
97
+ error: `That service-role key is actually the "${svcKey.role}" key. Paste the secret service_role key from Supabase → Project Settings → API.`,
98
+ };
99
+ }
100
+ const anonKeyInfo = inspectSupabaseKey(anonKey);
101
+ if (anonKeyInfo.role && anonKeyInfo.role !== 'anon') {
102
+ return {
103
+ ok: false,
104
+ error: `That anon key is actually the "${anonKeyInfo.role}" key. Paste the public anon key from Supabase → Project Settings → API.`,
105
+ };
106
+ }
107
+ let urlRef: string | undefined;
108
+ try {
109
+ const host = new URL(supabaseUrl).hostname;
110
+ if (host.endsWith('.supabase.co') || host.endsWith('.supabase.in')) {
111
+ urlRef = host.split('.')[0];
112
+ }
113
+ } catch {
114
+ // already validated above
115
+ }
116
+ if (urlRef && svcKey.ref && svcKey.ref !== urlRef) {
117
+ return {
118
+ ok: false,
119
+ error: `That service-role key belongs to project "${svcKey.ref}", but the URL is project "${urlRef}". Use keys from the same project.`,
120
+ };
121
+ }
122
+
123
+ if (!isLocalWritableEnv()) {
124
+ return {
125
+ ok: false,
126
+ error:
127
+ 'This environment is read-only. Set the Supabase variables on your hosting platform instead of here.',
128
+ };
129
+ }
130
+
131
+ // Validate the credentials with a service-role probe before writing anything.
132
+ const probe = createSupabaseJsClient(supabaseUrl, serviceRoleKey, {
133
+ auth: { persistSession: false, autoRefreshToken: false },
134
+ });
135
+
136
+ // Definitive service-role check: only the service_role can call the GoTrue admin API.
137
+ // A SELECT on site_settings can't tell service_role from anon (both can read it), so
138
+ // this catches a rotated/invalid key that the offline inspection above can't. Works on
139
+ // a fresh project too (the auth schema always exists, independent of the public schema).
140
+ try {
141
+ const { error: adminErr } = await probe.auth.admin.listUsers({ page: 1, perPage: 1 });
142
+ if (adminErr) {
143
+ return {
144
+ ok: false,
145
+ error: `That key can't perform admin actions (${adminErr.message}). Confirm you pasted the secret service_role key from Supabase → Project Settings → API.`,
146
+ };
147
+ }
148
+ } catch (caught) {
149
+ return {
150
+ ok: false,
151
+ error: `Could not verify the service-role key: ${
152
+ caught instanceof Error ? caught.message : 'unknown error'
153
+ }`,
154
+ };
155
+ }
156
+
157
+ let schemaReady = false;
158
+ try {
159
+ const { error } = await probe.from('site_settings').select('key').limit(1);
160
+ if (error) {
161
+ const missing = /relation|does not exist|schema cache/i.test(error.message);
162
+ if (!missing) {
163
+ return {
164
+ ok: false,
165
+ error: `Could not reach Supabase with those credentials: ${error.message}`,
166
+ };
167
+ }
168
+ schemaReady = false; // reachable, but the schema isn't applied yet
169
+ } else {
170
+ schemaReady = true;
171
+ }
172
+ } catch (caught) {
173
+ return {
174
+ ok: false,
175
+ error: `Could not connect to Supabase: ${
176
+ caught instanceof Error ? caught.message : 'unknown error'
177
+ }`,
178
+ };
179
+ }
180
+
181
+ const values: Record<string, string> = {
182
+ NEXT_PUBLIC_SUPABASE_URL: supabaseUrl,
183
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: anonKey,
184
+ SUPABASE_SERVICE_ROLE_KEY: serviceRoleKey,
185
+ // Enable the build-time migration hook for this install (Milestone 4). On Vercel
186
+ // VERCEL_ENV gates it instead; off-Vercel (local / Docker / standalone) production
187
+ // builds read this flag. Only written to a local-writable .env (no-op on Vercel).
188
+ NEXTBLOCK_BUILD_MIGRATE: '1',
189
+ };
190
+ if (input.siteUrl?.trim()) values.NEXT_PUBLIC_URL = input.siteUrl.trim();
191
+ if (input.postgresUrl?.trim()) values.POSTGRES_URL = input.postgresUrl.trim();
192
+ if (input.accessToken?.trim()) values.SUPABASE_ACCESS_TOKEN = input.accessToken.trim();
193
+
194
+ // Derive the project ref from the URL (https://<ref>.supabase.co) so the CLI schema
195
+ // step (`npm run db:migrate`) links + pushes to THIS project, not a stale one.
196
+ try {
197
+ const host = new URL(supabaseUrl).hostname;
198
+ if (host.endsWith('.supabase.co') || host.endsWith('.supabase.in')) {
199
+ values.SUPABASE_PROJECT_ID = host.split('.')[0];
200
+ }
201
+ } catch {
202
+ // already validated above; ignore
203
+ }
204
+
205
+ try {
206
+ await writeEnvLocal(values);
207
+ } catch (caught) {
208
+ return {
209
+ ok: false,
210
+ error: `Could not write .env.local: ${
211
+ caught instanceof Error ? caught.message : 'unknown error'
212
+ }`,
213
+ };
214
+ }
215
+
216
+ return {
217
+ ok: true,
218
+ schemaReady,
219
+ restartRecommended: true,
220
+ message: schemaReady
221
+ ? 'Connection saved and verified.'
222
+ : 'Connection saved. The database schema is not applied yet — run "npm run db:migrate", then re-check below.',
223
+ };
224
+ }
225
+
226
+ /** Poll provisioning status (used by the wizard to advance past connection/schema). */
227
+ export async function recheckStatus(): Promise<ProvisioningStatus & { writable: boolean }> {
228
+ const status = await getProvisioningStatus();
229
+ return { ...status, writable: isLocalWritableEnv() };
230
+ }
231
+
232
+ export interface CompleteSetupInput {
233
+ admin: { email: string; password: string; fullName: string };
234
+ /** Local-only extra env (media storage) collected by the wizard. */
235
+ envValues?: Record<string, string>;
236
+ /** "Start from a clean database" — wipe before installing (local dev only, server-gated). */
237
+ resetFirst?: boolean;
238
+ }
239
+
240
+ /**
241
+ * After a fresh migration, PostgREST may briefly not see the new tables (its schema
242
+ * cache). applyMigrations issues a reload, but it's async — poll until a known new
243
+ * table reads cleanly so the REST reads/writes below don't hit a false "table not
244
+ * found". Best-effort: gives up after ~6s and lets the caller proceed.
245
+ */
246
+ async function waitForSchemaCache(
247
+ supabase: ReturnType<typeof getServiceRoleSupabaseClient>,
248
+ ): Promise<void> {
249
+ for (let attempt = 0; attempt < 12; attempt++) {
250
+ const { error } = await supabase.from('system_configuration').select('id').limit(1);
251
+ if (!error) return;
252
+ await new Promise((resolve) => setTimeout(resolve, 500));
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Final step: persist remaining settings and create the first admin. The
258
+ * handle_new_user trigger assigns the ADMIN role and flips is_admin_created, so we
259
+ * never set the role ourselves. `email_confirm: true` means no SMTP round-trip is
260
+ * required, which keeps every channel (including cloud-without-SMTP) unblocked.
261
+ */
262
+ export async function completeSetup(input: CompleteSetupInput): Promise<ActionResult> {
263
+ // "Start from a clean database" (local dev only) deliberately re-installs over an
264
+ // existing DB — it wipes everything first — so the one-shot guard is skipped in that
265
+ // case. Otherwise enforce it: a provisioned instance can't be re-setup or wiped, and
266
+ // we return a clean message rather than throwing an unhandled error.
267
+ const willReset = input.resetFirst === true && isLocalWritableEnv();
268
+ if (!willReset) {
269
+ try {
270
+ await assertNotProvisioned();
271
+ } catch {
272
+ return {
273
+ ok: false,
274
+ error:
275
+ 'Setup has already been completed. Enable "Start from a clean database" to reinstall, or sign in instead.',
276
+ };
277
+ }
278
+ }
279
+
280
+ const email = input.admin?.email?.trim().toLowerCase();
281
+ const password = input.admin?.password ?? '';
282
+ const fullName = input.admin?.fullName?.trim() ?? '';
283
+
284
+ if (!email || !password) {
285
+ return { ok: false, error: 'Administrator email and password are required.' };
286
+ }
287
+ if (password.length < 8) {
288
+ return { ok: false, error: 'Use an administrator password of at least 8 characters.' };
289
+ }
290
+
291
+ // 1) Persist any local env extras (media storage) for Profile B.
292
+ if (
293
+ input.envValues &&
294
+ Object.keys(input.envValues).length > 0 &&
295
+ isLocalWritableEnv()
296
+ ) {
297
+ try {
298
+ await writeEnvLocal(input.envValues);
299
+ } catch (caught) {
300
+ return {
301
+ ok: false,
302
+ error: `Could not write .env.local: ${
303
+ caught instanceof Error ? caught.message : 'unknown error'
304
+ }`,
305
+ };
306
+ }
307
+ }
308
+
309
+ // 2) Service-role access is required from here on.
310
+ let admin: ReturnType<typeof getServiceRoleSupabaseClient>;
311
+ try {
312
+ admin = getServiceRoleSupabaseClient();
313
+ } catch {
314
+ return {
315
+ ok: false,
316
+ error:
317
+ 'The service-role key is not loaded yet. Restart the dev server after saving the connection, then retry.',
318
+ };
319
+ }
320
+
321
+ // 2.5) "Start from a clean database" (the fresh-install checkbox): wipe the DB (public
322
+ // schema + migration history + auth users) before installing, so each fresh setup
323
+ // starts clean. Two reliable guards: assertNotProvisioned() above means this only
324
+ // runs when no admin exists (a live site is immune), and isLocalWritableEnv()
325
+ // restricts it to local dev (a deployed app can never be tricked into wiping
326
+ // itself, even by a crafted request).
327
+ if (willReset) {
328
+ const reset = await resetDatabase();
329
+ if (!reset.ok) {
330
+ return { ok: false, error: `Could not reset the database: ${reset.error}` };
331
+ }
332
+ // Clear auth users via the admin API (reliable — SQL on the auth schema can be
333
+ // permission-restricted), so the admin email is free to reuse on this fresh install.
334
+ try {
335
+ const { data: list } = await admin.auth.admin.listUsers();
336
+ for (const existing of list?.users ?? []) {
337
+ await admin.auth.admin.deleteUser(existing.id);
338
+ }
339
+ } catch {
340
+ // Non-fatal — the createUser "already exists" fallback below also handles leftovers.
341
+ }
342
+ }
343
+
344
+ // 3) Apply the database schema if it isn't there yet (e.g. a fresh Supabase project).
345
+ // Direct Postgres connection — no CLI, no config.toml. Idempotent (tracks applied
346
+ // migrations), so it's a no-op when the schema already exists (Docker, or a re-run).
347
+ // Then wait for PostgREST to pick up the new tables before we touch them via REST.
348
+ const schemaStatus = await getProvisioningStatus();
349
+ if (!schemaStatus.schemaReady) {
350
+ // applyMigrations prefers the Supabase Management API (HTTPS, needs only the access
351
+ // token + project ref) and falls back to a direct Postgres connection. It returns a
352
+ // clear error if neither is available.
353
+ const schema = await applyMigrations();
354
+ if (!schema.ok) {
355
+ return { ok: false, error: `Could not apply the database schema: ${schema.error}` };
356
+ }
357
+ }
358
+
359
+ // Ensure PostgREST has the schema cached before ANY REST read/write below — covers a
360
+ // fresh apply AND a cold cache over a pre-existing schema (Docker boot race / re-run).
361
+ await waitForSchemaCache(admin);
362
+
363
+ // On the native Supabase Storage backend (zero-key Vercel deploy), provision the public
364
+ // media bucket now so the first upload doesn't have to. Best-effort — the upload path
365
+ // also ensures it lazily, so a transient failure here never blocks finishing setup.
366
+ if (getStorageBackend() === 'supabase') {
367
+ try {
368
+ await ensureStorageBucket();
369
+ } catch (caught) {
370
+ console.warn(
371
+ 'Could not pre-create the Supabase Storage media bucket (will retry on first upload):',
372
+ caught instanceof Error ? caught.message : caught,
373
+ );
374
+ }
375
+ }
376
+
377
+ // 4) Persist DB-backed settings (service role bypasses RLS — no admin exists yet).
378
+ // Sign-up policy, bot protection, email, and payments are no longer collected by the
379
+ // wizard; they are configured later from the CMS. New sign-ups default to requiring
380
+ // email verification (auto_accept_signups = false) as a safe default.
381
+ try {
382
+ await setSystemConfigurationServiceRole({ auto_accept_signups: false });
383
+ } catch (caught) {
384
+ return {
385
+ ok: false,
386
+ error: `Failed to save settings: ${
387
+ caught instanceof Error ? caught.message : 'unknown error'
388
+ }`,
389
+ };
390
+ }
391
+
392
+ // 4) Create the first admin account (already confirmed). The handle_new_user trigger
393
+ // assigns ADMIN to the very first user and flips is_admin_created atomically.
394
+ const createPayload = {
395
+ email,
396
+ password,
397
+ email_confirm: true,
398
+ user_metadata: { full_name: fullName },
399
+ };
400
+ let { data: created, error: createError } = await admin.auth.admin.createUser(createPayload);
401
+
402
+ // If a clean-install reset left a stale auth user (admin-API cleanup above hit an edge
403
+ // case), remove it and retry once so the operator can reuse their email.
404
+ if (
405
+ createError &&
406
+ input.resetFirst === true &&
407
+ /already|registered|exists/i.test(createError.message)
408
+ ) {
409
+ try {
410
+ const { data: list } = await admin.auth.admin.listUsers();
411
+ const stale = list?.users?.find((u) => u.email?.toLowerCase() === email);
412
+ if (stale) {
413
+ await admin.auth.admin.deleteUser(stale.id);
414
+ ({ data: created, error: createError } = await admin.auth.admin.createUser(createPayload));
415
+ }
416
+ } catch {
417
+ // fall through to the error handling below
418
+ }
419
+ }
420
+
421
+ if (createError || !created?.user) {
422
+ if (createError && /already|registered|exists/i.test(createError.message)) {
423
+ return {
424
+ ok: false,
425
+ error:
426
+ 'An account with this email already exists. Enable "Start from a clean database", or use a different email.',
427
+ };
428
+ }
429
+ return {
430
+ ok: false,
431
+ error: `Could not create the administrator account: ${
432
+ createError?.message ?? 'unknown error'
433
+ }`,
434
+ };
435
+ }
436
+
437
+ // Guard against a concurrent-setup race: assertNotProvisioned() is a check-then-act,
438
+ // so two simultaneous submissions could both pass it. The trigger still grants ADMIN
439
+ // to only the first user — so if this account came back as anything other than ADMIN,
440
+ // another session won the race. Undo this spurious account and report it, rather than
441
+ // signing the operator in as a non-admin.
442
+ const { data: createdProfile } = await admin
443
+ .from('profiles')
444
+ .select('role')
445
+ .eq('id', created.user.id)
446
+ .maybeSingle();
447
+ if (createdProfile?.role !== 'ADMIN') {
448
+ await admin.auth.admin.deleteUser(created.user.id).catch((err) => {
449
+ console.warn('Failed to clean up stale admin user after race condition:', err);
450
+ });
451
+ return {
452
+ ok: false,
453
+ error: 'Setup was just completed by another session. Please sign in instead.',
454
+ };
455
+ }
456
+
457
+ // The wizard establishes the session afterwards via the canonical signInAction (a more
458
+ // reliable cookie path than signing in here), so we just report success.
459
+ return { ok: true, message: 'Setup complete.' };
460
+ }