create-nextblock 0.16.1 → 0.16.3

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 (264) 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 -221
  26. package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -186
  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/route.ts +4 -4
  30. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +4542 -2904
  31. package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
  32. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  33. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  34. package/templates/nextblock-template/app/api/view/route.ts +114 -114
  35. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  36. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  37. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  38. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  39. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  40. package/templates/nextblock-template/app/checkout/page.tsx +57 -57
  41. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -558
  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 +406 -406
  46. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  47. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  48. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  49. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
  50. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  51. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  52. package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +40 -0
  53. package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -0
  54. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  55. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  56. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  57. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  58. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  59. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  60. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  61. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  62. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  63. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  64. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  65. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  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/components/PageForm.tsx +649 -649
  74. package/templates/nextblock-template/app/cms/pages/page.tsx +273 -227
  75. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +2 -0
  76. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +624 -618
  77. package/templates/nextblock-template/app/cms/posts/page.tsx +251 -194
  78. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  79. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  80. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  81. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  82. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  83. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  84. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  85. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  86. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  87. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  88. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  90. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  91. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  92. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  93. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  95. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  96. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  97. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  98. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  99. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  100. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  101. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  102. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  103. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  104. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
  105. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
  106. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
  107. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
  108. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
  109. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
  110. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
  111. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  112. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  113. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  114. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  115. package/templates/nextblock-template/app/layout.tsx +671 -671
  116. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  117. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  118. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  119. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  120. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  121. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  122. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  123. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  124. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  125. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  126. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  127. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  128. package/templates/nextblock-template/app/providers.tsx +96 -96
  129. package/templates/nextblock-template/app/robots.ts +123 -123
  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 -137
  181. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +251 -244
  182. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
  183. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
  184. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
  185. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
  186. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
  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 -86
  214. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
  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 -154
  225. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
  226. package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
  227. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
  228. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
  229. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +157 -140
  230. package/templates/nextblock-template/lib/seo/page-document.test.ts +384 -350
  231. package/templates/nextblock-template/lib/seo/page-document.ts +463 -412
  232. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
  233. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
  234. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
  235. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
  236. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
  237. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
  238. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -22
  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 +1 -2
  253. package/templates/nextblock-template/package.json +1 -1
  254. package/templates/nextblock-template/postcss.config.js +6 -6
  255. package/templates/nextblock-template/scripts/backup.js +115 -115
  256. package/templates/nextblock-template/scripts/restore.js +385 -385
  257. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  258. package/templates/nextblock-template/tailwind.config.js +25 -25
  259. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  260. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  261. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  262. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  263. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  264. package/tsconfig.base.json +3 -3
@@ -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
+ }