create-nextblock 0.16.1 → 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 (258) 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/sandboxResetSql.ts +3092 -3092
  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 -558
  41. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  42. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  43. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
  44. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +406 -406
  45. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
  46. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  47. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  48. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +254 -254
  49. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  50. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
  51. package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
  52. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
  53. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  54. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  55. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  56. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  57. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
  58. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
  59. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
  60. package/templates/nextblock-template/app/cms/layout.tsx +101 -101
  61. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  62. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  63. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  64. package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
  65. package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
  66. package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
  67. package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
  68. package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
  69. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  70. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +649 -649
  71. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +618 -618
  72. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
  73. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  74. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  75. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  76. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  77. package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
  78. package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
  79. package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
  80. package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
  81. package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
  82. package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
  83. package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
  84. package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
  85. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  86. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  87. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
  88. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  89. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  90. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  91. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
  92. package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
  93. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
  94. package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
  95. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  96. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  97. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  98. package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -514
  99. package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -529
  100. package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -57
  101. package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -448
  102. package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -93
  103. package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -46
  104. package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -47
  105. package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
  106. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  107. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  108. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  109. package/templates/nextblock-template/app/layout.tsx +671 -671
  110. package/templates/nextblock-template/app/lib/seo.ts +319 -319
  111. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  112. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  113. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
  114. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  115. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  116. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  117. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  118. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  119. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  120. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  121. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  122. package/templates/nextblock-template/app/providers.tsx +96 -96
  123. package/templates/nextblock-template/app/robots.ts +123 -123
  124. package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
  125. package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
  126. package/templates/nextblock-template/app/thread/layout.tsx +15 -15
  127. package/templates/nextblock-template/app/thread/page.tsx +98 -98
  128. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  129. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  130. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  131. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  132. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  133. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  134. package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
  135. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  136. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  137. package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
  138. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  139. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  140. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  141. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  142. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  143. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  144. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  145. package/templates/nextblock-template/components/Header.tsx +38 -38
  146. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  147. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  148. package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
  149. package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
  150. package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
  151. package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
  152. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  153. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  154. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  155. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  156. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  157. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  158. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  159. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
  160. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
  161. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
  162. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
  163. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  164. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
  165. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
  166. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
  167. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  168. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  169. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  170. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  171. package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
  172. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  173. package/templates/nextblock-template/components/form-message.tsx +32 -32
  174. package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -137
  175. package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -244
  176. package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -749
  177. package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -195
  178. package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -144
  179. package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -243
  180. package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -110
  181. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  182. package/templates/nextblock-template/components/theme-icon.tsx +78 -78
  183. package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
  184. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  185. package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
  186. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  187. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
  188. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
  189. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
  190. package/templates/nextblock-template/docs/README.md +42 -42
  191. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
  192. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  193. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  194. package/templates/nextblock-template/index.d.ts +7 -7
  195. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  196. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
  197. package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
  198. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
  199. package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
  200. package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
  201. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
  202. package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
  203. package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
  204. package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
  205. package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
  206. package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
  207. package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -86
  208. package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -60
  209. package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
  210. package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
  211. package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
  212. package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
  213. package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
  214. package/templates/nextblock-template/lib/messages/threads.ts +579 -579
  215. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  216. package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
  217. package/templates/nextblock-template/lib/search/types.ts +27 -27
  218. package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -154
  219. package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -109
  220. package/templates/nextblock-template/lib/seo/block-content.ts +123 -123
  221. package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -242
  222. package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -204
  223. package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -140
  224. package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -350
  225. package/templates/nextblock-template/lib/seo/page-document.ts +412 -412
  226. package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -479
  227. package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -466
  228. package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -102
  229. package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -41
  230. package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -370
  231. package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -510
  232. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -177
  233. package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
  234. package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
  235. package/templates/nextblock-template/lib/site-url.test.ts +89 -89
  236. package/templates/nextblock-template/lib/site-url.ts +102 -102
  237. package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
  238. package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
  239. package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
  240. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  241. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  242. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  243. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  244. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  245. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  246. package/templates/nextblock-template/next-env.d.ts +1 -2
  247. package/templates/nextblock-template/package.json +1 -1
  248. package/templates/nextblock-template/postcss.config.js +6 -6
  249. package/templates/nextblock-template/scripts/backup.js +115 -115
  250. package/templates/nextblock-template/scripts/restore.js +385 -385
  251. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  252. package/templates/nextblock-template/tailwind.config.js +25 -25
  253. package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
  254. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  255. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  256. package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
  257. package/templates/nextblock-template/tools/update.mjs +1303 -1303
  258. 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
+ }