create-nextblock 0.2.78 → 0.8.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 (413) hide show
  1. package/bin/create-nextblock.js +740 -459
  2. package/package.json +1 -2
  3. package/scripts/sync-template.js +18 -1
  4. package/templates/nextblock-template/.browserslistrc +11 -0
  5. package/templates/nextblock-template/.swcrc +30 -30
  6. package/templates/nextblock-template/README.md +23 -114
  7. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
  8. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
  9. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
  10. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
  11. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
  14. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
  15. package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
  16. package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
  17. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
  18. package/templates/nextblock-template/app/actions/consent.ts +57 -0
  19. package/templates/nextblock-template/app/actions/formActions.ts +130 -11
  20. package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
  21. package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
  22. package/templates/nextblock-template/app/actions/postActions.ts +146 -48
  23. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
  24. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
  25. package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
  26. package/templates/nextblock-template/app/actions.ts +67 -12
  27. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
  28. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
  29. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
  30. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
  31. package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
  32. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
  33. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
  35. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
  36. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
  37. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
  38. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
  39. package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
  40. package/templates/nextblock-template/app/api/draft/route.ts +93 -0
  41. package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
  42. package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -0
  46. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
  47. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
  48. package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
  49. package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
  50. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
  51. package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
  52. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
  53. package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
  54. package/templates/nextblock-template/app/cart/page.tsx +7 -0
  55. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
  56. package/templates/nextblock-template/app/checkout/page.tsx +52 -0
  57. package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
  58. package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
  59. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
  60. package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
  61. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
  62. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
  63. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
  64. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
  65. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
  66. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
  67. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
  68. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
  69. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
  70. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
  71. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
  72. package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
  73. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
  74. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
  75. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  76. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
  77. package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
  78. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
  79. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
  80. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
  81. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
  82. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
  83. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
  84. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
  85. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
  86. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
  87. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
  88. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
  89. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
  90. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
  91. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
  92. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
  93. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
  94. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
  95. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
  96. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
  97. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
  98. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
  99. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
  100. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
  101. package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
  102. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
  103. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
  104. package/templates/nextblock-template/app/cms/layout.tsx +29 -10
  105. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
  106. package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
  107. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
  108. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  109. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
  110. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
  111. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
  112. package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
  113. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
  114. package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
  115. package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
  116. package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
  117. package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
  118. package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
  119. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
  120. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
  121. package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
  122. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
  123. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
  124. package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
  125. package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
  126. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
  127. package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
  128. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
  129. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
  130. package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
  131. package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
  132. package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
  133. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
  134. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
  135. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
  136. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
  137. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
  138. package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
  139. package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
  140. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
  141. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
  142. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
  143. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
  144. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
  145. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
  146. package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
  147. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
  148. package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
  149. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
  150. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
  151. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
  152. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
  153. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
  154. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
  155. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
  156. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
  157. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
  158. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
  159. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
  160. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
  161. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
  162. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
  163. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
  164. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
  165. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
  166. package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
  167. package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
  168. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
  169. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
  170. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
  171. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
  172. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  173. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
  174. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
  175. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
  176. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
  177. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
  178. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  179. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
  180. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
  181. package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
  182. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
  183. package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
  184. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
  185. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
  186. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
  187. package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
  188. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
  189. package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
  190. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
  191. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
  192. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
  193. package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
  194. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
  195. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
  196. package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
  197. package/templates/nextblock-template/app/globals.css +9 -0
  198. package/templates/nextblock-template/app/layout.tsx +372 -120
  199. package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
  200. package/templates/nextblock-template/app/lib/seo.ts +279 -0
  201. package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
  202. package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
  203. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
  204. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
  205. package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
  206. package/templates/nextblock-template/app/page.tsx +165 -73
  207. package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
  208. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
  209. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
  210. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
  211. package/templates/nextblock-template/app/profile/account-data.ts +37 -0
  212. package/templates/nextblock-template/app/profile/account-links.ts +22 -0
  213. package/templates/nextblock-template/app/profile/account-types.ts +11 -0
  214. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
  215. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
  216. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
  217. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
  218. package/templates/nextblock-template/app/profile/page.tsx +51 -0
  219. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
  220. package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
  221. package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
  222. package/templates/nextblock-template/app/providers.tsx +55 -17
  223. package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
  224. package/templates/nextblock-template/app/sitemap.ts +128 -0
  225. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
  226. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
  227. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
  228. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
  229. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
  230. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
  231. package/templates/nextblock-template/components/AppShell.tsx +154 -0
  232. package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
  233. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
  234. package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
  235. package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
  236. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
  237. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
  238. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
  239. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
  240. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
  241. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
  242. package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
  243. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
  244. package/templates/nextblock-template/components/Header.tsx +49 -41
  245. package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
  246. package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
  247. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
  248. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
  249. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
  250. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
  251. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
  252. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
  253. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
  254. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
  255. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
  256. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
  257. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
  258. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
  259. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
  260. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
  261. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
  262. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
  263. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
  264. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
  265. package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
  266. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
  267. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
  268. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
  269. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
  270. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
  271. package/templates/nextblock-template/components/blocks/types.ts +7 -6
  272. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  273. package/templates/nextblock-template/components/form-message.tsx +32 -26
  274. package/templates/nextblock-template/components/header-auth.tsx +69 -17
  275. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
  276. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
  277. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
  278. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
  279. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
  280. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  281. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
  282. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
  283. package/templates/nextblock-template/context/AuthContext.tsx +23 -90
  284. package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
  285. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  286. package/templates/nextblock-template/context/language-rest-client.ts +31 -0
  287. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
  288. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
  289. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
  290. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
  291. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
  292. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
  293. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
  294. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
  295. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
  296. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
  297. package/templates/nextblock-template/docs/README.md +34 -0
  298. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
  299. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
  300. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
  301. package/templates/nextblock-template/index.d.ts +2 -0
  302. package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
  303. package/templates/nextblock-template/lib/ai-client.ts +247 -0
  304. package/templates/nextblock-template/lib/ai-config.ts +81 -0
  305. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
  306. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
  307. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
  308. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
  309. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
  310. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
  311. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
  312. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
  313. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
  314. package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
  315. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
  316. package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
  317. package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
  318. package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
  319. package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
  320. package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
  321. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
  322. package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
  323. package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
  324. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
  325. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
  326. package/templates/nextblock-template/lib/blocks/README.md +13 -670
  327. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
  328. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
  329. package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
  330. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
  331. package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
  332. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
  333. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
  334. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
  335. package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
  336. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
  337. package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
  338. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
  339. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
  340. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
  341. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
  342. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
  343. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
  344. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
  345. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
  346. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
  347. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
  348. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
  349. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
  350. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
  351. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
  352. package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
  353. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
  354. package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
  355. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
  356. package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
  357. package/templates/nextblock-template/lib/privacy/types.ts +67 -0
  358. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
  359. package/templates/nextblock-template/lib/promotions/server.ts +741 -0
  360. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
  361. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
  362. package/templates/nextblock-template/lib/search/server.ts +585 -0
  363. package/templates/nextblock-template/lib/search/types.ts +27 -0
  364. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
  365. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
  366. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
  367. package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
  368. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
  369. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
  370. package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
  371. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
  372. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
  373. package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
  374. package/templates/nextblock-template/lib/zod-config.ts +5 -0
  375. package/templates/nextblock-template/next.config.js +190 -66
  376. package/templates/nextblock-template/package.json +34 -30
  377. package/templates/nextblock-template/proxy.ts +435 -253
  378. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  379. package/templates/nextblock-template/public/images/cap.webp +0 -0
  380. package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
  381. package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
  382. package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
  383. package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
  384. package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
  385. package/templates/nextblock-template/public/images/extensibility.webp +0 -0
  386. package/templates/nextblock-template/public/images/goals.webp +0 -0
  387. package/templates/nextblock-template/public/images/included.webp +0 -0
  388. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  389. package/templates/nextblock-template/public/images/pants.webp +0 -0
  390. package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
  391. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
  392. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
  393. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
  394. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
  395. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
  396. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
  397. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
  398. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
  399. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
  400. package/templates/nextblock-template/tailwind.config.js +1 -0
  401. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
  402. package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
  403. package/templates/nextblock-template/tsconfig.json +52 -66
  404. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  405. package/templates/nextblock-template/types/jsdom.d.ts +6 -0
  406. package/templates/nextblock-template/app/force-styles.tsx +0 -31
  407. package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
  408. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
  409. package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
  410. package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
  411. package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
  412. package/templates/nextblock-template/docs/files-structure.md +0 -426
  413. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
@@ -0,0 +1,522 @@
1
+ import { APICallError } from 'ai';
2
+
3
+ export const CORTEX_AI_OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
4
+ export const CORTEX_AI_OPENROUTER_FREE_ROUTER_MODEL = 'openrouter/free';
5
+
6
+ export const CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY = [
7
+ 'qwen/qwen3-next-80b-a3b-instruct:free',
8
+ 'nvidia/nemotron-3-super-120b-a12b:free',
9
+ 'nvidia/nemotron-nano-9b-v2:free',
10
+ ] as const;
11
+
12
+ export const CORTEX_AI_REQUIRED_MODEL_PARAMETERS = ['tools', 'structured_outputs'] as const;
13
+
14
+ const CORTEX_AI_OPTIONAL_MODEL_PARAMETER_MAP = {
15
+ frequencyPenalty: 'frequency_penalty',
16
+ logitBias: 'logit_bias',
17
+ presencePenalty: 'presence_penalty',
18
+ seed: 'seed',
19
+ stopSequences: 'stop',
20
+ temperature: 'temperature',
21
+ topK: 'top_k',
22
+ topP: 'top_p',
23
+ } as const;
24
+
25
+ export const CORTEX_AI_MODEL_REGISTRY = {
26
+ defaultFreeRouter: CORTEX_AI_OPENROUTER_FREE_ROUTER_MODEL,
27
+ defaultStructuredOutputModel: CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY[0],
28
+ defaultToolCallingModel: CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY[0],
29
+ freeFallbacks: CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY,
30
+ structuredJsonPreferred: CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY,
31
+ toolCallingPreferred: CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY,
32
+ } as const;
33
+
34
+ export type CortexAiOpenRouterModelId =
35
+ | typeof CORTEX_AI_OPENROUTER_FREE_ROUTER_MODEL
36
+ | (typeof CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY)[number]
37
+ | (string & {});
38
+
39
+ export type CortexAiRoutingCredentialSource = 'env' | 'manual' | 'stored';
40
+
41
+ export type CortexAiOpenRouterModelPricing = Record<string, string>;
42
+
43
+ export type CortexAiCompatibleOpenRouterModel = {
44
+ contextLength: number | null;
45
+ created: number | null;
46
+ expirationDate: string | null;
47
+ id: CortexAiOpenRouterModelId;
48
+ name: string;
49
+ pricing: CortexAiOpenRouterModelPricing;
50
+ supportedParameters: readonly string[];
51
+ };
52
+
53
+ export type CortexAiStoredModelSelection = {
54
+ contextLength: number | null;
55
+ modelId: CortexAiOpenRouterModelId;
56
+ name: string;
57
+ pricing: CortexAiOpenRouterModelPricing;
58
+ supportedParameters: readonly string[];
59
+ updatedAt: string;
60
+ };
61
+
62
+ export type CortexAiRoutingPolicy = {
63
+ credentialSource: CortexAiRoutingCredentialSource;
64
+ ignoredRequestedModelId: CortexAiOpenRouterModelId | null;
65
+ modelIds: readonly CortexAiOpenRouterModelId[];
66
+ modelSelection: CortexAiStoredModelSelection | null;
67
+ };
68
+
69
+ export type CortexAiModelAttempt = {
70
+ errorMessage?: string;
71
+ modelId: CortexAiOpenRouterModelId;
72
+ rateLimited: boolean;
73
+ status: 'success' | 'rate_limited' | 'retried' | 'failed';
74
+ };
75
+
76
+ export class CortexAiRoutingError extends Error {
77
+ readonly attempts: readonly CortexAiModelAttempt[];
78
+
79
+ constructor(message: string, attempts: readonly CortexAiModelAttempt[], cause?: unknown) {
80
+ super(message);
81
+ this.name = 'CortexAiRoutingError';
82
+ this.attempts = attempts;
83
+ this.cause = cause;
84
+ }
85
+ }
86
+
87
+ function uniqueModelIds(modelIds: readonly CortexAiOpenRouterModelId[]) {
88
+ return Array.from(new Set(modelIds.filter(Boolean)));
89
+ }
90
+
91
+ function readRecord(value: unknown): Record<string, unknown> | null {
92
+ return value && typeof value === 'object' && !Array.isArray(value)
93
+ ? (value as Record<string, unknown>)
94
+ : null;
95
+ }
96
+
97
+ function readString(value: unknown) {
98
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
99
+ }
100
+
101
+ function readStringArray(value: unknown) {
102
+ return Array.isArray(value)
103
+ ? value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
104
+ : [];
105
+ }
106
+
107
+ function readNumberLike(value: unknown) {
108
+ const parsed = typeof value === 'number' ? value : Number(value);
109
+ return Number.isFinite(parsed) ? parsed : null;
110
+ }
111
+
112
+ function readStringRecord(value: unknown): CortexAiOpenRouterModelPricing {
113
+ const record = readRecord(value);
114
+
115
+ if (!record) {
116
+ return {};
117
+ }
118
+
119
+ return Object.fromEntries(
120
+ Object.entries(record)
121
+ .filter(([, entryValue]) => entryValue !== null && entryValue !== undefined)
122
+ .map(([key, entryValue]) => [key, String(entryValue)])
123
+ );
124
+ }
125
+
126
+ function supportsRequiredModelParameters(supportedParameters: readonly string[]) {
127
+ const supported = new Set(supportedParameters);
128
+ return CORTEX_AI_REQUIRED_MODEL_PARAMETERS.every((parameter) => supported.has(parameter));
129
+ }
130
+
131
+ function isTextOutputModel(record: Record<string, unknown>) {
132
+ const architecture = readRecord(record.architecture);
133
+ return readStringArray(architecture?.output_modalities).includes('text');
134
+ }
135
+
136
+ function getExpirationTimestamp(value: unknown) {
137
+ if (value === null || value === undefined) {
138
+ return null;
139
+ }
140
+
141
+ if (typeof value === 'number') {
142
+ return value > 10_000_000_000 ? value : value * 1000;
143
+ }
144
+
145
+ if (typeof value === 'string' && value.trim()) {
146
+ const parsed = Date.parse(value);
147
+ return Number.isFinite(parsed) ? parsed : null;
148
+ }
149
+
150
+ return null;
151
+ }
152
+
153
+ function isExpiredOpenRouterModel(value: unknown, now: Date) {
154
+ const expirationTimestamp = getExpirationTimestamp(value);
155
+ return expirationTimestamp !== null && expirationTimestamp <= now.getTime();
156
+ }
157
+
158
+ function readOpenRouterModelContextLength(record: Record<string, unknown>) {
159
+ const topProvider = readRecord(record.top_provider);
160
+ return (
161
+ readNumberLike(record.context_length) ??
162
+ readNumberLike(record.contextLength) ??
163
+ readNumberLike(topProvider?.context_length) ??
164
+ null
165
+ );
166
+ }
167
+
168
+ function readOpenRouterModelExpirationDate(record: Record<string, unknown>) {
169
+ const expirationDate = record.expiration_date ?? record.expirationDate;
170
+ return typeof expirationDate === 'string' && expirationDate.trim()
171
+ ? expirationDate.trim()
172
+ : null;
173
+ }
174
+
175
+ export function isCortexAiFreeModelId(modelId: CortexAiOpenRouterModelId | null | undefined) {
176
+ return Boolean(
177
+ modelId &&
178
+ (CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY as readonly string[]).includes(modelId)
179
+ );
180
+ }
181
+
182
+ export function safeParseCortexAiModelSelection(
183
+ value: unknown
184
+ ): CortexAiStoredModelSelection | null {
185
+ const record = readRecord(value);
186
+
187
+ if (!record) {
188
+ return null;
189
+ }
190
+
191
+ const modelId = readString(record.modelId);
192
+ const name = readString(record.name);
193
+ const supportedParameters = readStringArray(record.supportedParameters);
194
+ const updatedAt = readString(record.updatedAt);
195
+
196
+ if (!modelId || !name || !updatedAt || !supportsRequiredModelParameters(supportedParameters)) {
197
+ return null;
198
+ }
199
+
200
+ return {
201
+ contextLength: readNumberLike(record.contextLength),
202
+ modelId,
203
+ name,
204
+ pricing: readStringRecord(record.pricing),
205
+ supportedParameters,
206
+ updatedAt,
207
+ };
208
+ }
209
+
210
+ export function createCortexAiStoredModelSelection(
211
+ model: CortexAiCompatibleOpenRouterModel,
212
+ now = new Date()
213
+ ): CortexAiStoredModelSelection {
214
+ return {
215
+ contextLength: model.contextLength,
216
+ modelId: model.id,
217
+ name: model.name,
218
+ pricing: model.pricing,
219
+ supportedParameters: [...model.supportedParameters],
220
+ updatedAt: now.toISOString(),
221
+ };
222
+ }
223
+
224
+ export function filterCortexAiCompatibleOpenRouterModels(
225
+ value: unknown,
226
+ now = new Date()
227
+ ): CortexAiCompatibleOpenRouterModel[] {
228
+ const root = readRecord(value);
229
+ const rawModels = Array.isArray(value)
230
+ ? value
231
+ : Array.isArray(root?.data)
232
+ ? root.data
233
+ : [];
234
+
235
+ const compatibleModels: CortexAiCompatibleOpenRouterModel[] = [];
236
+
237
+ for (const rawModel of rawModels) {
238
+ const record = readRecord(rawModel);
239
+ const id = readString(record?.id);
240
+ const name = readString(record?.name);
241
+ const supportedParameters = readStringArray(record?.supported_parameters);
242
+
243
+ if (
244
+ !record ||
245
+ !id ||
246
+ !name ||
247
+ !isTextOutputModel(record) ||
248
+ isExpiredOpenRouterModel(record.expiration_date, now) ||
249
+ !supportsRequiredModelParameters(supportedParameters)
250
+ ) {
251
+ continue;
252
+ }
253
+
254
+ compatibleModels.push({
255
+ contextLength: readOpenRouterModelContextLength(record),
256
+ created: readNumberLike(record.created),
257
+ expirationDate: readOpenRouterModelExpirationDate(record),
258
+ id,
259
+ name,
260
+ pricing: readStringRecord(record.pricing),
261
+ supportedParameters,
262
+ });
263
+ }
264
+
265
+ return compatibleModels.sort((left, right) => left.name.localeCompare(right.name));
266
+ }
267
+
268
+ export function buildCortexAiModelFallbackChain(params?: {
269
+ fallbackModelIds?: readonly CortexAiOpenRouterModelId[];
270
+ modelId?: CortexAiOpenRouterModelId | null;
271
+ }) {
272
+ return uniqueModelIds([
273
+ params?.modelId || CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY[0],
274
+ ...(params?.fallbackModelIds || CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY),
275
+ ]);
276
+ }
277
+
278
+ export function buildCortexAiRoutingPolicy(params: {
279
+ credentialSource: CortexAiRoutingCredentialSource;
280
+ fallbackModelIds?: readonly CortexAiOpenRouterModelId[];
281
+ requestedModelId?: CortexAiOpenRouterModelId | null;
282
+ selectedModel?: CortexAiStoredModelSelection | null;
283
+ }): CortexAiRoutingPolicy {
284
+ const requestedModelId = params.requestedModelId?.trim() || null;
285
+
286
+ if (params.credentialSource === 'env') {
287
+ return {
288
+ credentialSource: params.credentialSource,
289
+ ignoredRequestedModelId:
290
+ requestedModelId && !isCortexAiFreeModelId(requestedModelId)
291
+ ? requestedModelId
292
+ : null,
293
+ modelIds: [...CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY],
294
+ modelSelection: null,
295
+ };
296
+ }
297
+
298
+ const fallbackModelIds = uniqueModelIds(
299
+ params.fallbackModelIds?.length
300
+ ? params.fallbackModelIds
301
+ : CORTEX_AI_FREE_MODEL_FALLBACK_REGISTRY
302
+ );
303
+ const preferredModelId =
304
+ params.credentialSource === 'stored'
305
+ ? params.selectedModel?.modelId || fallbackModelIds[0]
306
+ : requestedModelId || params.selectedModel?.modelId || fallbackModelIds[0];
307
+
308
+ return {
309
+ credentialSource: params.credentialSource,
310
+ ignoredRequestedModelId:
311
+ params.credentialSource === 'stored' &&
312
+ requestedModelId &&
313
+ requestedModelId !== preferredModelId
314
+ ? requestedModelId
315
+ : null,
316
+ modelIds: uniqueModelIds([preferredModelId, ...fallbackModelIds]),
317
+ modelSelection: params.selectedModel || null,
318
+ };
319
+ }
320
+
321
+ export function omitUnsupportedCortexAiModelOptions<TOptions extends Record<string, unknown>>(
322
+ options: TOptions,
323
+ params: {
324
+ modelId: CortexAiOpenRouterModelId;
325
+ modelSelection?: CortexAiStoredModelSelection | null;
326
+ }
327
+ ): TOptions {
328
+ const modelSelection = params.modelSelection;
329
+
330
+ if (!modelSelection || modelSelection.modelId !== params.modelId) {
331
+ return options;
332
+ }
333
+
334
+ const supportedParameters = new Set(modelSelection.supportedParameters);
335
+ const unsupportedOptionKeys = Object.entries(CORTEX_AI_OPTIONAL_MODEL_PARAMETER_MAP)
336
+ .filter(([optionKey, parameterName]) => optionKey in options && !supportedParameters.has(parameterName))
337
+ .map(([optionKey]) => optionKey);
338
+
339
+ if (unsupportedOptionKeys.length === 0) {
340
+ return options;
341
+ }
342
+
343
+ const nextOptions = { ...options };
344
+
345
+ for (const optionKey of unsupportedOptionKeys) {
346
+ delete nextOptions[optionKey as keyof TOptions];
347
+ }
348
+
349
+ return nextOptions;
350
+ }
351
+
352
+ function readNumericProperty(value: unknown, property: string) {
353
+ if (!value || typeof value !== 'object' || !(property in value)) {
354
+ return null;
355
+ }
356
+
357
+ const raw = (value as Record<string, unknown>)[property];
358
+ const parsed = typeof raw === 'number' ? raw : Number(raw);
359
+ return Number.isFinite(parsed) ? parsed : null;
360
+ }
361
+
362
+ export function getHttpStatusCode(error: unknown): number | null {
363
+ if (APICallError.isInstance(error)) {
364
+ return error.statusCode ?? null;
365
+ }
366
+
367
+ const directStatus = readNumericProperty(error, 'statusCode') ?? readNumericProperty(error, 'status');
368
+
369
+ if (directStatus) {
370
+ return directStatus;
371
+ }
372
+
373
+ if (error && typeof error === 'object' && 'response' in error) {
374
+ const response = (error as { response?: unknown }).response;
375
+ const responseStatus = readNumericProperty(response, 'status');
376
+
377
+ if (responseStatus) {
378
+ return responseStatus;
379
+ }
380
+ }
381
+
382
+ if (error && typeof error === 'object' && 'cause' in error) {
383
+ return getHttpStatusCode((error as { cause?: unknown }).cause);
384
+ }
385
+
386
+ return null;
387
+ }
388
+
389
+ export function isOpenRouterRateLimitError(error: unknown) {
390
+ return getHttpStatusCode(error) === 429;
391
+ }
392
+
393
+ function getDeepErrorMessage(error: unknown): string {
394
+ if (!error) {
395
+ return '';
396
+ }
397
+
398
+ if (error instanceof Error) {
399
+ const causeMessage = 'cause' in error ? getDeepErrorMessage(error.cause) : '';
400
+ return [error.message, causeMessage].filter(Boolean).join('\n');
401
+ }
402
+
403
+ if (typeof error === 'object') {
404
+ const record = error as Record<string, unknown>;
405
+ return ['message', 'error', 'text', 'cause']
406
+ .map((key) => getDeepErrorMessage(record[key]))
407
+ .filter(Boolean)
408
+ .join('\n');
409
+ }
410
+
411
+ return String(error);
412
+ }
413
+
414
+ export function isOpenRouterRecoverableRoutingError(error: unknown) {
415
+ if (isOpenRouterRateLimitError(error)) {
416
+ return true;
417
+ }
418
+
419
+ return /No endpoints found|no longer available|not available as a free model|transitioned to a paid model/i.test(
420
+ getDeepErrorMessage(error)
421
+ );
422
+ }
423
+
424
+ function truncateErrorMessage(message: string, maxLength = 900) {
425
+ const normalized = message.replace(/\s+/g, ' ').trim();
426
+ return normalized.length > maxLength
427
+ ? `${normalized.slice(0, maxLength - 1).trimEnd()}...`
428
+ : normalized;
429
+ }
430
+
431
+ function getErrorMessage(error: unknown) {
432
+ const message = getDeepErrorMessage(error);
433
+ return message ? truncateErrorMessage(message) : 'Unknown OpenRouter error.';
434
+ }
435
+
436
+ export function summarizeCortexAiRoutingError(
437
+ error: unknown,
438
+ fallbackMessage = 'Cortex AI request failed.'
439
+ ) {
440
+ if (error instanceof CortexAiRoutingError) {
441
+ const attemptMessages = error.attempts
442
+ .map((attempt) => attempt.errorMessage)
443
+ .filter((message): message is string => Boolean(message?.trim()));
444
+ const firstAttemptMessage = attemptMessages[0];
445
+ const lastAttemptMessage = attemptMessages[attemptMessages.length - 1];
446
+ const causeMessage = truncateErrorMessage(getDeepErrorMessage(error.cause));
447
+
448
+ if (
449
+ firstAttemptMessage &&
450
+ lastAttemptMessage &&
451
+ firstAttemptMessage !== lastAttemptMessage
452
+ ) {
453
+ return `First model error: ${firstAttemptMessage} Last model error: ${lastAttemptMessage}`;
454
+ }
455
+
456
+ return lastAttemptMessage || firstAttemptMessage || causeMessage || error.message;
457
+ }
458
+
459
+ return truncateErrorMessage(getDeepErrorMessage(error)) || fallbackMessage;
460
+ }
461
+
462
+ export async function runWithCortexAiModelFallback<T>(params: {
463
+ execute: (modelId: CortexAiOpenRouterModelId) => Promise<T>;
464
+ modelIds: readonly CortexAiOpenRouterModelId[];
465
+ shouldRetry?: (error: unknown) => boolean;
466
+ }): Promise<{
467
+ attempts: readonly CortexAiModelAttempt[];
468
+ modelId: CortexAiOpenRouterModelId;
469
+ result: T;
470
+ }> {
471
+ const modelIds = uniqueModelIds(params.modelIds);
472
+ const shouldRetry = params.shouldRetry || isOpenRouterRecoverableRoutingError;
473
+ let attempts: readonly CortexAiModelAttempt[] = [];
474
+ let lastError: unknown = null;
475
+
476
+ for (const modelId of modelIds) {
477
+ try {
478
+ const result = await params.execute(modelId);
479
+ attempts = [
480
+ ...attempts,
481
+ {
482
+ modelId,
483
+ rateLimited: false,
484
+ status: 'success',
485
+ },
486
+ ];
487
+
488
+ return {
489
+ attempts,
490
+ modelId,
491
+ result,
492
+ };
493
+ } catch (error) {
494
+ const rateLimited = isOpenRouterRateLimitError(error);
495
+ const retryable = shouldRetry(error);
496
+ lastError = error;
497
+ attempts = [
498
+ ...attempts,
499
+ {
500
+ errorMessage: getErrorMessage(error),
501
+ modelId,
502
+ rateLimited,
503
+ status: rateLimited ? 'rate_limited' : retryable ? 'retried' : 'failed',
504
+ },
505
+ ];
506
+
507
+ if (!retryable) {
508
+ throw new CortexAiRoutingError(
509
+ `OpenRouter request failed for model "${modelId}".`,
510
+ attempts,
511
+ error
512
+ );
513
+ }
514
+ }
515
+ }
516
+
517
+ throw new CortexAiRoutingError(
518
+ 'OpenRouter fallback exhausted all configured Cortex AI models.',
519
+ attempts,
520
+ lastError
521
+ );
522
+ }
@@ -0,0 +1,47 @@
1
+ // Server-only cookie helpers for the 2FA / trusted-device flow.
2
+ // All three cookies are HttpOnly so they are never readable from JS.
3
+ import { cookies } from 'next/headers';
4
+
5
+ /** Long-lived "remember this device" token (raw token; its SHA-256 is in the DB). */
6
+ export const TRUSTED_DEVICE_COOKIE = 'nb_trusted_device';
7
+ /** Signed marker proving the email second factor was satisfied this session. */
8
+ export const TWO_FACTOR_COOKIE = 'nb_2fa_verified';
9
+ /** Short-lived flag carrying the "remember me" checkbox from login to post-2FA. */
10
+ export const REMEMBER_INTENT_COOKIE = 'nb_remember_intent';
11
+
12
+ const isProd = process.env.NODE_ENV === 'production';
13
+
14
+ export async function setSecureCookie(
15
+ name: string,
16
+ value: string,
17
+ maxAgeSeconds: number,
18
+ ): Promise<void> {
19
+ const store = await cookies();
20
+ store.set({
21
+ name,
22
+ value,
23
+ httpOnly: true,
24
+ secure: isProd,
25
+ sameSite: 'lax',
26
+ path: '/',
27
+ maxAge: maxAgeSeconds,
28
+ });
29
+ }
30
+
31
+ export async function getCookieValue(name: string): Promise<string | null> {
32
+ const store = await cookies();
33
+ return store.get(name)?.value ?? null;
34
+ }
35
+
36
+ export async function clearCookie(name: string): Promise<void> {
37
+ const store = await cookies();
38
+ store.set({
39
+ name,
40
+ value: '',
41
+ httpOnly: true,
42
+ secure: isProd,
43
+ sameSite: 'lax',
44
+ path: '/',
45
+ maxAge: 0,
46
+ });
47
+ }
@@ -0,0 +1,42 @@
1
+ // Server-only crypto helpers for 2FA tokens, trusted-device hashing, and the
2
+ // signed "second factor satisfied" session marker. Uses Node's crypto module.
3
+ import crypto from 'node:crypto';
4
+
5
+ /** SHA-256 hex digest. Used to store device/code hashes (never the raw value). */
6
+ export function sha256Hex(input: string): string {
7
+ return crypto.createHash('sha256').update(input).digest('hex');
8
+ }
9
+
10
+ /** Cryptographically random URL-safe token (default 32 bytes -> 43 chars). */
11
+ export function randomToken(bytes = 32): string {
12
+ return crypto.randomBytes(bytes).toString('base64url');
13
+ }
14
+
15
+ /** Random fixed-length numeric code, e.g. a 6-digit email 2FA code. */
16
+ export function generateNumericCode(digits = 6): string {
17
+ const max = 10 ** digits;
18
+ return crypto.randomInt(0, max).toString().padStart(digits, '0');
19
+ }
20
+
21
+ function getSecret(): string {
22
+ // Dedicated secret if provided, otherwise fall back to the service-role key
23
+ // (server-only, never shipped to the client).
24
+ const secret = process.env.NB_2FA_SECRET || process.env.SUPABASE_SERVICE_ROLE_KEY;
25
+ if (!secret) {
26
+ throw new Error('Missing NB_2FA_SECRET / SUPABASE_SERVICE_ROLE_KEY for 2FA signing.');
27
+ }
28
+ return secret;
29
+ }
30
+
31
+ /** HMAC-SHA256 signature (base64url) of an arbitrary payload string. */
32
+ export function hmacSign(payload: string): string {
33
+ return crypto.createHmac('sha256', getSecret()).update(payload).digest('base64url');
34
+ }
35
+
36
+ /** Constant-time string comparison that never throws on length mismatch. */
37
+ export function safeEqual(a: string, b: string): boolean {
38
+ const ab = Buffer.from(a);
39
+ const bb = Buffer.from(b);
40
+ if (ab.length !== bb.length) return false;
41
+ return crypto.timingSafeEqual(ab, bb);
42
+ }
@@ -0,0 +1,92 @@
1
+ // Server-only trusted-device ("Remember this device") helpers.
2
+ //
3
+ // Trust is server-validated: the cookie holds only a random token, and a bypass
4
+ // is honoured ONLY when a non-expired user_trusted_devices row matches its
5
+ // SHA-256. Deleting the row instantly revokes trust, regardless of the cookie.
6
+ import { createClient, getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
7
+ import { getSecuritySettings } from '../privacy/settings';
8
+ import { randomToken, sha256Hex } from './crypto';
9
+ import {
10
+ TRUSTED_DEVICE_COOKIE,
11
+ clearCookie,
12
+ getCookieValue,
13
+ setSecureCookie,
14
+ } from './cookies';
15
+
16
+ export interface TrustedDeviceRow {
17
+ id: string;
18
+ browser_metadata: string | null;
19
+ created_at: string;
20
+ expires_at: string;
21
+ }
22
+
23
+ /** Mint a trusted-device token, persist its hash, and set the cookie. */
24
+ export async function issueTrustedDevice(
25
+ userId: string,
26
+ browserMetadata: string | null,
27
+ ): Promise<void> {
28
+ const { trusted_device_days } = await getSecuritySettings();
29
+ const maxAgeSeconds = trusted_device_days * 24 * 60 * 60;
30
+ const token = randomToken(32);
31
+ const expiresAt = new Date(Date.now() + maxAgeSeconds * 1000).toISOString();
32
+
33
+ const svc = getServiceRoleSupabaseClient();
34
+ const { error } = await svc.from('user_trusted_devices').insert({
35
+ user_id: userId,
36
+ device_hash: sha256Hex(token),
37
+ browser_metadata: browserMetadata ? browserMetadata.slice(0, 500) : null,
38
+ expires_at: expiresAt,
39
+ });
40
+ if (error) {
41
+ console.error('Failed to persist trusted device:', error.message);
42
+ return; // Non-fatal: the user simply isn't remembered.
43
+ }
44
+ await setSecureCookie(TRUSTED_DEVICE_COOKIE, token, maxAgeSeconds);
45
+ }
46
+
47
+ /** True when the current request carries a cookie matching a live trusted row. */
48
+ export async function hasValidTrustedDevice(userId: string): Promise<boolean> {
49
+ const token = await getCookieValue(TRUSTED_DEVICE_COOKIE);
50
+ if (!token) return false;
51
+ const svc = getServiceRoleSupabaseClient();
52
+ const { data } = await svc
53
+ .from('user_trusted_devices')
54
+ .select('id')
55
+ .eq('user_id', userId)
56
+ .eq('device_hash', sha256Hex(token))
57
+ .gt('expires_at', new Date().toISOString())
58
+ .maybeSingle();
59
+ return Boolean(data);
60
+ }
61
+
62
+ /** List the signed-in user's trusted devices (RLS scopes to their own rows). */
63
+ export async function listTrustedDevices(userId: string): Promise<TrustedDeviceRow[]> {
64
+ const supabase = createClient();
65
+ const { data } = await supabase
66
+ .from('user_trusted_devices')
67
+ .select('id, browser_metadata, created_at, expires_at')
68
+ .eq('user_id', userId)
69
+ .order('created_at', { ascending: false });
70
+ return data ?? [];
71
+ }
72
+
73
+ /** Revoke one trusted device the user owns (RLS prevents touching others'). */
74
+ export async function revokeTrustedDevice(userId: string, id: string): Promise<void> {
75
+ const supabase = createClient();
76
+ const { error } = await supabase
77
+ .from('user_trusted_devices')
78
+ .delete()
79
+ .eq('id', id)
80
+ .eq('user_id', userId);
81
+ if (error) {
82
+ console.error('Failed to revoke trusted device:', error.message);
83
+ throw new Error('Failed to revoke device.');
84
+ }
85
+ }
86
+
87
+ /** Drop all of a user's trusted devices and the local cookie (e.g. on MFA disable). */
88
+ export async function revokeAllTrustedDevices(userId: string): Promise<void> {
89
+ const svc = getServiceRoleSupabaseClient();
90
+ await svc.from('user_trusted_devices').delete().eq('user_id', userId);
91
+ await clearCookie(TRUSTED_DEVICE_COOKIE);
92
+ }