create-nextblock 0.15.10 → 0.16.2

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 (266) hide show
  1. package/docker-template/.dockerignore +1 -1
  2. package/libs/db/tsconfig.lib.json +3 -3
  3. package/libs/editor/tsconfig.lib.json +3 -3
  4. package/libs/ui/tsconfig.lib.json +3 -3
  5. package/libs/utils/tsconfig.json +3 -3
  6. package/package.json +50 -50
  7. package/project.json +19 -19
  8. package/templates/nextblock-template/.dockerignore +1 -1
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/AGENTS.md +9 -9
  11. package/templates/nextblock-template/CLAUDE.md +1 -1
  12. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  13. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  14. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  15. package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
  16. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  17. package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
  18. package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
  19. package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
  20. package/templates/nextblock-template/app/actions/email.ts +241 -241
  21. package/templates/nextblock-template/app/actions/formActions.ts +245 -245
  22. package/templates/nextblock-template/app/actions/interactions.ts +489 -489
  23. package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
  24. package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
  25. package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -0
  26. package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -0
  27. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  28. package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
  29. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -2900
  30. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  31. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  32. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  33. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  34. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  35. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  37. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  38. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  39. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  40. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -552
  41. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +27 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  43. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  44. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
  45. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +180 -3
  46. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +171 -6
  47. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  48. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  49. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  50. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +12 -3
  51. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  52. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  53. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  54. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  55. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  56. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  57. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  58. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  59. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  60. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  62. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  63. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  64. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  65. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +177 -2
  66. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  67. package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
  68. package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
  69. package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
  70. package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
  71. package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
  72. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  73. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +28 -0
  74. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +248 -5
  75. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +33 -0
  76. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +240 -5
  77. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  78. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  79. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  80. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  81. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  82. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  83. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  84. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  85. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  86. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  87. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  88. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  90. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  91. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  92. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  93. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  95. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  96. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  97. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  98. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  99. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  100. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  101. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  102. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  103. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -0
  104. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -0
  105. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -0
  106. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -0
  107. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -0
  108. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -0
  109. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -0
  110. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  111. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  112. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  113. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  114. package/templates/nextblock-template/app/layout.tsx +671 -671
  115. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  116. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  117. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  118. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  119. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  120. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  121. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  122. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  123. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  124. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  125. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  126. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  127. package/templates/nextblock-template/app/providers.tsx +96 -96
  128. package/templates/nextblock-template/app/robots.ts +123 -0
  129. package/templates/nextblock-template/app/sitemap.ts +1 -1
  130. package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
  131. package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
  132. package/templates/nextblock-template/app/thread/layout.tsx +15 -15
  133. package/templates/nextblock-template/app/thread/page.tsx +98 -98
  134. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  135. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  136. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  137. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  138. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  139. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  140. package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
  141. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  142. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  143. package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
  144. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  145. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  146. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  147. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  148. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  149. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  150. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  151. package/templates/nextblock-template/components/Header.tsx +38 -38
  152. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  153. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  154. package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
  155. package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
  156. package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
  157. package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
  158. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  159. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  160. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  161. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  162. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  163. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  164. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  165. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
  166. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
  167. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
  168. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
  169. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  170. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
  171. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
  172. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
  173. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  174. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  175. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  176. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  177. package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
  178. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  179. package/templates/nextblock-template/components/form-message.tsx +32 -32
  180. package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -0
  181. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -0
  182. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -0
  183. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -0
  184. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -0
  185. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -0
  186. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -0
  187. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  188. package/templates/nextblock-template/components/theme-icon.tsx +78 -78
  189. package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
  190. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  191. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
  192. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  193. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
  194. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
  195. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
  196. package/templates/nextblock-template/docs/README.md +42 -42
  197. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
  198. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  199. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  200. package/templates/nextblock-template/index.d.ts +7 -7
  201. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  202. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
  203. package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
  204. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
  205. package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
  206. package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
  207. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
  208. package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
  209. package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
  210. package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
  211. package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
  212. package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
  213. package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -0
  214. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -0
  215. package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
  216. package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
  217. package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
  218. package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
  219. package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
  220. package/templates/nextblock-template/lib/messages/threads.ts +579 -579
  221. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  222. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
  223. package/templates/nextblock-template/lib/search/types.ts +27 -27
  224. package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -0
  225. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -0
  226. package/templates/nextblock-template/lib/seo/block-content.ts +123 -0
  227. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -0
  228. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -0
  229. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -0
  230. package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -0
  231. package/templates/nextblock-template/lib/seo/page-document.ts +412 -0
  232. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -0
  233. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -0
  234. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -0
  235. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -0
  236. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -0
  237. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -0
  238. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -167
  239. package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
  240. package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
  241. package/templates/nextblock-template/lib/site-url.test.ts +89 -89
  242. package/templates/nextblock-template/lib/site-url.ts +102 -102
  243. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
  244. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
  245. package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
  246. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  247. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  248. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  249. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  250. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  251. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  252. package/templates/nextblock-template/next-env.d.ts +0 -1
  253. package/templates/nextblock-template/package.json +1 -1
  254. package/templates/nextblock-template/postcss.config.js +6 -6
  255. package/templates/nextblock-template/proxy.ts +240 -20
  256. package/templates/nextblock-template/scripts/backup.js +115 -115
  257. package/templates/nextblock-template/scripts/restore.js +385 -385
  258. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  259. package/templates/nextblock-template/tailwind.config.js +25 -25
  260. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  261. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  262. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  263. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  264. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  265. package/tsconfig.base.json +3 -3
  266. package/templates/nextblock-template/app/robots.txt/route.ts +0 -32
@@ -1,20 +1,20 @@
1
- /**
2
- * Stamp the request's CSP nonce onto inline <script> tags inside block HTML.
3
- *
4
- * Rich-text blocks may legitimately carry an inline <script> (an editor or Cortex AI
5
- * adding a small animation or widget). The site's CSP lists a nonce in `script-src`,
6
- * and per CSP Level 2 a browser IGNORES `'unsafe-inline'` once a nonce or hash is
7
- * present — so an un-nonced inline script is silently blocked, with no server-side
8
- * symptom. Adding the nonce is what makes authored JS actually run.
9
- *
10
- * Only inline scripts are touched: a `src=` script loads a remote file and is
11
- * governed by the host allowlist instead, and re-stamping one that already carries a
12
- * nonce would corrupt it.
13
- */
14
- export function addNonceToInlineScripts(html: string, nonce: string): string {
15
- if (!html || !nonce) return html || '';
16
-
17
- return html.replace(/<script(?![^>]*\bsrc=)([^>]*)(?<!nonce=["'][^"']*["'])>/gi, (_match, attrs) => {
18
- return `<script nonce="${nonce}"${attrs}>`;
19
- });
20
- }
1
+ /**
2
+ * Stamp the request's CSP nonce onto inline <script> tags inside block HTML.
3
+ *
4
+ * Rich-text blocks may legitimately carry an inline <script> (an editor or Cortex AI
5
+ * adding a small animation or widget). The site's CSP lists a nonce in `script-src`,
6
+ * and per CSP Level 2 a browser IGNORES `'unsafe-inline'` once a nonce or hash is
7
+ * present — so an un-nonced inline script is silently blocked, with no server-side
8
+ * symptom. Adding the nonce is what makes authored JS actually run.
9
+ *
10
+ * Only inline scripts are touched: a `src=` script loads a remote file and is
11
+ * governed by the host allowlist instead, and re-stamping one that already carries a
12
+ * nonce would corrupt it.
13
+ */
14
+ export function addNonceToInlineScripts(html: string, nonce: string): string {
15
+ if (!html || !nonce) return html || '';
16
+
17
+ return html.replace(/<script(?![^>]*\bsrc=)([^>]*)(?<!nonce=["'][^"']*["'])>/gi, (_match, attrs) => {
18
+ return `<script nonce="${nonce}"${attrs}>`;
19
+ });
20
+ }
@@ -1,64 +1,64 @@
1
- import 'server-only';
2
-
3
- import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
4
-
5
- import { isPlaceholderEmail, usableEmail } from '../email/placeholder-address';
6
- import { resolveSellerContactEmail } from '../commerce/seller-contact';
7
-
8
- /**
9
- * A contact form that appears to work but delivers nowhere.
10
- *
11
- * The starter content ships a contact page whose form is addressed to
12
- * `contact@example.com`. That is the worst kind of default: every layer downstream treats
13
- * it as a real setting, the visitor is thanked, the transport accepts the message, and it
14
- * is delivered to a domain RFC 2606 reserves so that it can never exist. There is no
15
- * error anywhere for the operator to notice — which is exactly how a site runs for months
16
- * quietly discarding enquiries.
17
- *
18
- * Submissions are still recorded as threads, so nothing is actually lost. But the owner
19
- * is not being told about them, and they have no way to know that.
20
- */
21
- export interface ContactReminder {
22
- /** Forms addressed to a reserved placeholder domain. */
23
- placeholderForms: Array<{ formKey: string; label: string; recipient: string }>;
24
- /** True when nothing anywhere resolves to a deliverable address. */
25
- noFallback: boolean;
26
- }
27
-
28
- export async function getContactReminder(): Promise<ContactReminder | null> {
29
- // The sandbox is re-seeded on a schedule, so its placeholder is expected and permanent.
30
- if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') return null;
31
-
32
- try {
33
- const supabase = getServiceRoleSupabaseClient();
34
- const { data: endpoints } = await supabase
35
- .from('form_endpoints')
36
- .select('form_key, label, recipient_email');
37
-
38
- const placeholderForms = (endpoints ?? [])
39
- .filter((endpoint) => isPlaceholderEmail(endpoint.recipient_email))
40
- .map((endpoint) => ({
41
- formKey: endpoint.form_key,
42
- label: endpoint.label || 'Contact form',
43
- recipient: endpoint.recipient_email as string,
44
- }));
45
-
46
- // Forms with no deliverable address of their own lean on the site-wide ladder.
47
- // Whether a missing fallback matters depends entirely on whether anything needs it:
48
- // an install whose every form carries its own real address is correctly configured,
49
- // and warning it about an empty fallback would be noise.
50
- const dependsOnFallback = (endpoints ?? []).some(
51
- (endpoint) => !usableEmail(endpoint.recipient_email)
52
- );
53
-
54
- const { email: fallback } = await resolveSellerContactEmail();
55
- const noFallback = dependsOnFallback && !fallback;
56
-
57
- if (placeholderForms.length === 0 && !noFallback) return null;
58
-
59
- return { placeholderForms, noFallback };
60
- } catch {
61
- // Never let a reminder break the CMS shell.
62
- return null;
63
- }
64
- }
1
+ import 'server-only';
2
+
3
+ import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
4
+
5
+ import { isPlaceholderEmail, usableEmail } from '../email/placeholder-address';
6
+ import { resolveSellerContactEmail } from '../commerce/seller-contact';
7
+
8
+ /**
9
+ * A contact form that appears to work but delivers nowhere.
10
+ *
11
+ * The starter content ships a contact page whose form is addressed to
12
+ * `contact@example.com`. That is the worst kind of default: every layer downstream treats
13
+ * it as a real setting, the visitor is thanked, the transport accepts the message, and it
14
+ * is delivered to a domain RFC 2606 reserves so that it can never exist. There is no
15
+ * error anywhere for the operator to notice — which is exactly how a site runs for months
16
+ * quietly discarding enquiries.
17
+ *
18
+ * Submissions are still recorded as threads, so nothing is actually lost. But the owner
19
+ * is not being told about them, and they have no way to know that.
20
+ */
21
+ export interface ContactReminder {
22
+ /** Forms addressed to a reserved placeholder domain. */
23
+ placeholderForms: Array<{ formKey: string; label: string; recipient: string }>;
24
+ /** True when nothing anywhere resolves to a deliverable address. */
25
+ noFallback: boolean;
26
+ }
27
+
28
+ export async function getContactReminder(): Promise<ContactReminder | null> {
29
+ // The sandbox is re-seeded on a schedule, so its placeholder is expected and permanent.
30
+ if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') return null;
31
+
32
+ try {
33
+ const supabase = getServiceRoleSupabaseClient();
34
+ const { data: endpoints } = await supabase
35
+ .from('form_endpoints')
36
+ .select('form_key, label, recipient_email');
37
+
38
+ const placeholderForms = (endpoints ?? [])
39
+ .filter((endpoint) => isPlaceholderEmail(endpoint.recipient_email))
40
+ .map((endpoint) => ({
41
+ formKey: endpoint.form_key,
42
+ label: endpoint.label || 'Contact form',
43
+ recipient: endpoint.recipient_email as string,
44
+ }));
45
+
46
+ // Forms with no deliverable address of their own lean on the site-wide ladder.
47
+ // Whether a missing fallback matters depends entirely on whether anything needs it:
48
+ // an install whose every form carries its own real address is correctly configured,
49
+ // and warning it about an empty fallback would be noise.
50
+ const dependsOnFallback = (endpoints ?? []).some(
51
+ (endpoint) => !usableEmail(endpoint.recipient_email)
52
+ );
53
+
54
+ const { email: fallback } = await resolveSellerContactEmail();
55
+ const noFallback = dependsOnFallback && !fallback;
56
+
57
+ if (placeholderForms.length === 0 && !noFallback) return null;
58
+
59
+ return { placeholderForms, noFallback };
60
+ } catch {
61
+ // Never let a reminder break the CMS shell.
62
+ return null;
63
+ }
64
+ }
@@ -0,0 +1,135 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ type ProductRow = {
4
+ product_type: 'physical' | 'digital';
5
+ status: 'active' | 'draft';
6
+ };
7
+
8
+ const mocks = vi.hoisted(() => ({
9
+ getStoreReadiness: vi.fn(),
10
+ verifyPackageOnline: vi.fn(),
11
+ products: [] as ProductRow[],
12
+ }));
13
+
14
+ vi.mock('server-only', () => ({}));
15
+
16
+ vi.mock('@nextblock-cms/ecommerce/server', () => ({
17
+ getStoreReadiness: mocks.getStoreReadiness,
18
+ }));
19
+
20
+ vi.mock('@nextblock-cms/db/server', () => ({
21
+ verifyPackageOnline: mocks.verifyPackageOnline,
22
+ createClient: () => ({
23
+ auth: {
24
+ getUser: async () => ({ data: { user: { id: 'admin-1' } } }),
25
+ },
26
+ from: (table: string) => {
27
+ if (table === 'profiles') {
28
+ return {
29
+ select: () => ({
30
+ eq: () => ({
31
+ maybeSingle: async () => ({ data: { role: 'ADMIN' } }),
32
+ }),
33
+ }),
34
+ };
35
+ }
36
+
37
+ if (table === 'products') {
38
+ return {
39
+ select: () => {
40
+ const filters: Array<(product: ProductRow) => boolean> = [];
41
+ const query = {
42
+ eq(column: keyof ProductRow, value: string) {
43
+ filters.push((product) => product[column] === value);
44
+ return query;
45
+ },
46
+ then(onFulfilled: (value: { count: number }) => unknown) {
47
+ const count = mocks.products.filter((product) =>
48
+ filters.every((filter) => filter(product)),
49
+ ).length;
50
+ return Promise.resolve({ count }).then(onFulfilled);
51
+ },
52
+ };
53
+ return query;
54
+ },
55
+ };
56
+ }
57
+
58
+ throw new Error(`Unexpected table in test: ${table}`);
59
+ },
60
+ }),
61
+ }));
62
+
63
+ import { getPaymentsReminder } from './payments-reminder';
64
+
65
+ function providerReadiness({
66
+ stripeReady,
67
+ freemiusReady,
68
+ }: {
69
+ stripeReady: boolean;
70
+ freemiusReady: boolean;
71
+ }) {
72
+ return {
73
+ stripe: {
74
+ provider: 'stripe',
75
+ label: 'Stripe',
76
+ enabled: true,
77
+ hasKeys: stripeReady,
78
+ ready: stripeReady,
79
+ missing: stripeReady ? [] : ['Secret key'],
80
+ },
81
+ freemius: {
82
+ provider: 'freemius',
83
+ label: 'Freemius',
84
+ enabled: true,
85
+ hasKeys: freemiusReady,
86
+ ready: freemiusReady,
87
+ missing: freemiusReady ? [] : ['Secret key'],
88
+ },
89
+ };
90
+ }
91
+
92
+ describe('CMS payments reminder', () => {
93
+ beforeEach(() => {
94
+ vi.clearAllMocks();
95
+ mocks.products.length = 0;
96
+ mocks.verifyPackageOnline.mockResolvedValue(true);
97
+ });
98
+
99
+ it('ignores an unconfigured Stripe toggle when the store has only digital products', async () => {
100
+ mocks.products.push({ product_type: 'digital', status: 'active' });
101
+ mocks.getStoreReadiness.mockResolvedValue(
102
+ providerReadiness({ stripeReady: false, freemiusReady: true }),
103
+ );
104
+
105
+ await expect(getPaymentsReminder()).resolves.toBeNull();
106
+ });
107
+
108
+ it('warns about Stripe when any physical product needs it', async () => {
109
+ mocks.products.push({ product_type: 'physical', status: 'draft' });
110
+ mocks.getStoreReadiness.mockResolvedValue(
111
+ providerReadiness({ stripeReady: false, freemiusReady: true }),
112
+ );
113
+
114
+ await expect(getPaymentsReminder()).resolves.toEqual({
115
+ blocked: [
116
+ { provider: 'stripe', label: 'Stripe', missing: ['Secret key'] },
117
+ ],
118
+ affectedProducts: 0,
119
+ });
120
+ });
121
+
122
+ it('warns about Freemius when a digital product needs it', async () => {
123
+ mocks.products.push({ product_type: 'digital', status: 'active' });
124
+ mocks.getStoreReadiness.mockResolvedValue(
125
+ providerReadiness({ stripeReady: true, freemiusReady: false }),
126
+ );
127
+
128
+ await expect(getPaymentsReminder()).resolves.toEqual({
129
+ blocked: [
130
+ { provider: 'freemius', label: 'Freemius', missing: ['Secret key'] },
131
+ ],
132
+ affectedProducts: 1,
133
+ });
134
+ });
135
+ });
@@ -11,7 +11,11 @@ import { getStoreReadiness } from '@nextblock-cms/ecommerce/server';
11
11
  */
12
12
  export interface PaymentsReminder {
13
13
  /** Providers that are not ready, with what each is missing. */
14
- blocked: Array<{ provider: 'stripe' | 'freemius'; label: string; missing: string[] }>;
14
+ blocked: Array<{
15
+ provider: 'stripe' | 'freemius';
16
+ label: string;
17
+ missing: string[];
18
+ }>;
15
19
  /** Published products that currently cannot be bought because of the above. */
16
20
  affectedProducts: number;
17
21
  }
@@ -51,33 +55,33 @@ export async function getPaymentsReminder(): Promise<PaymentsReminder | null> {
51
55
 
52
56
  if (unready.length === 0) return null;
53
57
 
54
- // A provider the store does not sell through is not a problem to be nagged about.
55
- // Both providers ship DISABLED (baseline seed), so filtering on readiness alone
56
- // would show a permanent "Freemius still needs setting up" banner to every
57
- // physical-goods shop that had finished its Stripe setup correctly.
58
- //
59
- // "In use" means the admin switched the provider on, or the catalogue already
60
- // contains products that would need it — counted across ALL statuses, so a shop
61
- // building a draft catalogue is warned before it publishes rather than after.
58
+ // A provider matters to this reminder only when the catalogue contains products
59
+ // of the type routed through it. Do not treat a stale/accidental provider toggle as
60
+ // usage: that produced a Stripe warning in digital-only stores with no Stripe keys.
61
+ // Count across ALL statuses so a shop building a draft catalogue is warned before
62
+ // it publishes rather than after.
62
63
  const usage = await Promise.all(
63
64
  unready.map(async (entry) => {
64
- const [{ count: totalCount }, { count: activeCount }] = await Promise.all([
65
- supabase
66
- .from('products')
67
- .select('id', { count: 'exact', head: true })
68
- .eq('payment_provider', entry.provider),
69
- supabase
70
- .from('products')
71
- .select('id', { count: 'exact', head: true })
72
- .eq('status', 'active')
73
- .eq('payment_provider', entry.provider),
74
- ]);
65
+ const productType =
66
+ entry.provider === 'stripe' ? 'physical' : 'digital';
67
+ const [{ count: totalCount }, { count: activeCount }] =
68
+ await Promise.all([
69
+ supabase
70
+ .from('products')
71
+ .select('id', { count: 'exact', head: true })
72
+ .eq('product_type', productType),
73
+ supabase
74
+ .from('products')
75
+ .select('id', { count: 'exact', head: true })
76
+ .eq('status', 'active')
77
+ .eq('product_type', productType),
78
+ ]);
75
79
  return {
76
80
  entry,
77
- inUse: entry.enabled || (totalCount ?? 0) > 0,
81
+ inUse: (totalCount ?? 0) > 0,
78
82
  activeProducts: activeCount ?? 0,
79
83
  };
80
- })
84
+ }),
81
85
  );
82
86
 
83
87
  const relevant = usage.filter((item) => item.inUse);
@@ -89,7 +93,10 @@ export async function getPaymentsReminder(): Promise<PaymentsReminder | null> {
89
93
  label: entry.label,
90
94
  missing: entry.missing,
91
95
  })),
92
- affectedProducts: relevant.reduce((total, item) => total + item.activeProducts, 0),
96
+ affectedProducts: relevant.reduce(
97
+ (total, item) => total + item.activeProducts,
98
+ 0,
99
+ ),
93
100
  };
94
101
  } catch {
95
102
  // Never let a reminder break the CMS shell.
@@ -1,42 +1,42 @@
1
- import 'server-only';
2
-
3
- import { createClient, getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
4
-
5
- /**
6
- * How many messages want attention, for the CMS nav badge.
7
- *
8
- * Best-effort in the same shape as the system-alerts loader: any failure — including
9
- * the tables not existing yet on an install that has not run migration 27 — resolves to
10
- * zero rather than breaking the whole CMS chrome.
11
- *
12
- * Note the two halves count different things. Threads have a real `unread_for_admin`
13
- * flag. Interactions have no per-user read marker anywhere in the schema, so "pending
14
- * moderation" stands in for it — which means two admins working the queue see the same
15
- * number until one of them acts.
16
- */
17
- export async function getUnreadMessageCount(isAdmin: boolean): Promise<number> {
18
- try {
19
- let total = 0;
20
-
21
- if (isAdmin) {
22
- const service = getServiceRoleSupabaseClient();
23
- const { count } = await service
24
- .from('message_threads')
25
- .select('id', { count: 'exact', head: true })
26
- .eq('unread_for_admin', true);
27
- total += count ?? 0;
28
- }
29
-
30
- const supabase = createClient();
31
- const { count: pending } = await supabase
32
- .from('cms_interactions')
33
- .select('id', { count: 'exact', head: true })
34
- .eq('status', 'pending')
35
- .is('parent_id', null);
36
- total += pending ?? 0;
37
-
38
- return total;
39
- } catch {
40
- return 0;
41
- }
42
- }
1
+ import 'server-only';
2
+
3
+ import { createClient, getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
4
+
5
+ /**
6
+ * How many messages want attention, for the CMS nav badge.
7
+ *
8
+ * Best-effort in the same shape as the system-alerts loader: any failure — including
9
+ * the tables not existing yet on an install that has not run migration 27 — resolves to
10
+ * zero rather than breaking the whole CMS chrome.
11
+ *
12
+ * Note the two halves count different things. Threads have a real `unread_for_admin`
13
+ * flag. Interactions have no per-user read marker anywhere in the schema, so "pending
14
+ * moderation" stands in for it — which means two admins working the queue see the same
15
+ * number until one of them acts.
16
+ */
17
+ export async function getUnreadMessageCount(isAdmin: boolean): Promise<number> {
18
+ try {
19
+ let total = 0;
20
+
21
+ if (isAdmin) {
22
+ const service = getServiceRoleSupabaseClient();
23
+ const { count } = await service
24
+ .from('message_threads')
25
+ .select('id', { count: 'exact', head: true })
26
+ .eq('unread_for_admin', true);
27
+ total += count ?? 0;
28
+ }
29
+
30
+ const supabase = createClient();
31
+ const { count: pending } = await supabase
32
+ .from('cms_interactions')
33
+ .select('id', { count: 'exact', head: true })
34
+ .eq('status', 'pending')
35
+ .is('parent_id', null);
36
+ total += pending ?? 0;
37
+
38
+ return total;
39
+ } catch {
40
+ return 0;
41
+ }
42
+ }