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.
- package/docker-template/.dockerignore +1 -1
- package/libs/db/tsconfig.lib.json +3 -3
- package/libs/editor/tsconfig.lib.json +3 -3
- package/libs/ui/tsconfig.lib.json +3 -3
- package/libs/utils/tsconfig.json +3 -3
- package/package.json +50 -50
- package/project.json +19 -19
- package/templates/nextblock-template/.dockerignore +1 -1
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/AGENTS.md +9 -9
- package/templates/nextblock-template/CLAUDE.md +1 -1
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/ToasterProvider.tsx +26 -26
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -280
- package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -222
- package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -62
- package/templates/nextblock-template/app/actions/email.ts +241 -241
- package/templates/nextblock-template/app/actions/formActions.ts +245 -245
- package/templates/nextblock-template/app/actions/interactions.ts +489 -489
- package/templates/nextblock-template/app/actions/threadActions.ts +166 -166
- package/templates/nextblock-template/app/actions/visibilityActions.ts +210 -210
- package/templates/nextblock-template/app/api/ai/seo/alt-text/route.ts +221 -0
- package/templates/nextblock-template/app/api/ai/seo/metadata/route.ts +186 -0
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/checkout/route.ts +162 -162
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +3092 -2900
- package/templates/nextblock-template/app/api/mcp/route.ts +415 -415
- package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
- package/templates/nextblock-template/app/api/search/route.ts +43 -43
- package/templates/nextblock-template/app/api/view/route.ts +114 -114
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
- package/templates/nextblock-template/app/cart/page.tsx +7 -7
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/checkout/page.tsx +57 -57
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +558 -552
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +27 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -304
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +180 -3
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +171 -6
- package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -75
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +12 -3
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/VisibilityBadge.tsx +62 -62
- package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -542
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -66
- package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/layout.tsx +101 -101
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +177 -2
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -661
- package/templates/nextblock-template/app/cms/messages/actions.ts +404 -404
- package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -333
- package/templates/nextblock-template/app/cms/messages/page.tsx +87 -87
- package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -37
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +28 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +248 -5
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +33 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +240 -5
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -370
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
- package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
- package/templates/nextblock-template/app/cms/revisions/actions.ts +332 -332
- package/templates/nextblock-template/app/cms/revisions/service.test.ts +498 -498
- package/templates/nextblock-template/app/cms/revisions/service.ts +569 -569
- package/templates/nextblock-template/app/cms/revisions/utils.ts +304 -304
- package/templates/nextblock-template/app/cms/settings/cortex-ai/CortexAiSettingsClient.tsx +948 -948
- package/templates/nextblock-template/app/cms/settings/cortex-ai/McpServerSettingsCard.tsx +628 -628
- package/templates/nextblock-template/app/cms/settings/cortex-ai/mcp-actions.ts +230 -230
- package/templates/nextblock-template/app/cms/settings/cortex-ai/require-admin.ts +34 -34
- package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
- package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -227
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeEditor.tsx +382 -382
- package/templates/nextblock-template/app/cms/settings/global-css/components/ThemeManager.tsx +267 -267
- package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +40 -40
- package/templates/nextblock-template/app/cms/settings/global-css/theme-actions.ts +259 -259
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
- package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/seo/RedirectsCard.tsx +514 -0
- package/templates/nextblock-template/app/cms/settings/seo/RobotsCard.tsx +529 -0
- package/templates/nextblock-template/app/cms/settings/seo/SeoSettingsClient.tsx +57 -0
- package/templates/nextblock-template/app/cms/settings/seo/actions.ts +448 -0
- package/templates/nextblock-template/app/cms/settings/seo/mappers.ts +93 -0
- package/templates/nextblock-template/app/cms/settings/seo/page.tsx +46 -0
- package/templates/nextblock-template/app/cms/settings/seo/require-admin.ts +47 -0
- package/templates/nextblock-template/app/cms/settings/site-scripts/page.tsx +51 -51
- package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
- package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/layout.tsx +671 -671
- package/templates/nextblock-template/app/lib/seo.ts +319 -319
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -502
- package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
- package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
- package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
- package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
- package/templates/nextblock-template/app/providers.tsx +96 -96
- package/templates/nextblock-template/app/robots.ts +123 -0
- package/templates/nextblock-template/app/sitemap.ts +1 -1
- package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -164
- package/templates/nextblock-template/app/thread/[token]/route.ts +57 -57
- package/templates/nextblock-template/app/thread/layout.tsx +15 -15
- package/templates/nextblock-template/app/thread/page.tsx +98 -98
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/BlockRenderer.tsx +312 -312
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -188
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
- package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -378
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -426
- package/templates/nextblock-template/components/SiteScripts.tsx +56 -56
- package/templates/nextblock-template/components/StaffReplies.tsx +102 -102
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -18
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -20
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -25
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -385
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -157
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -34
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -612
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -32
- package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
- package/templates/nextblock-template/components/form-message.tsx +32 -32
- package/templates/nextblock-template/components/seo/GenerateMetaButton.tsx +137 -0
- package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +244 -0
- package/templates/nextblock-template/components/seo/SeoAuditPanel.tsx +749 -0
- package/templates/nextblock-template/components/seo/SeoIssueList.tsx +195 -0
- package/templates/nextblock-template/components/seo/SeoScoreDial.tsx +144 -0
- package/templates/nextblock-template/components/seo/SocialPreview.tsx +243 -0
- package/templates/nextblock-template/components/seo/SocialPreviewDialog.tsx +110 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-icon.tsx +78 -78
- package/templates/nextblock-template/components/theme-switcher.tsx +85 -85
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/ThemeCatalogContext.tsx +44 -44
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +77 -77
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +372 -372
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -309
- package/templates/nextblock-template/docs/README.md +42 -42
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12506 -12506
- package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
- package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
- package/templates/nextblock-template/index.d.ts +7 -7
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +134 -134
- package/templates/nextblock-template/lib/blocks/blockColors.ts +175 -175
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -761
- package/templates/nextblock-template/lib/blocks/inlineScriptNonce.ts +20 -20
- package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -64
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -0
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +30 -23
- package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -42
- package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -162
- package/templates/nextblock-template/lib/config/email-settings.ts +323 -323
- package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -57
- package/templates/nextblock-template/lib/cortex-ai/alt-text-request.ts +86 -0
- package/templates/nextblock-template/lib/cortex-ai/sandbox-headers.ts +60 -0
- package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -59
- package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -39
- package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -70
- package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -93
- package/templates/nextblock-template/lib/messages/thread-token.ts +157 -157
- package/templates/nextblock-template/lib/messages/threads.ts +579 -579
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/publishing/viewUrl.ts +26 -26
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/seo/alt-text-write-back.test.ts +154 -0
- package/templates/nextblock-template/lib/seo/alt-text-write-back.ts +109 -0
- package/templates/nextblock-template/lib/seo/block-content.ts +123 -0
- package/templates/nextblock-template/lib/seo/fix-prompts.test.ts +242 -0
- package/templates/nextblock-template/lib/seo/fix-prompts.ts +204 -0
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +140 -0
- package/templates/nextblock-template/lib/seo/page-document.test.ts +350 -0
- package/templates/nextblock-template/lib/seo/page-document.ts +412 -0
- package/templates/nextblock-template/lib/seo/redirect-store.test.ts +479 -0
- package/templates/nextblock-template/lib/seo/redirect-store.ts +466 -0
- package/templates/nextblock-template/lib/seo/robots-settings-signature.test.ts +102 -0
- package/templates/nextblock-template/lib/seo/robots-settings-signature.ts +41 -0
- package/templates/nextblock-template/lib/seo/robots-txt.test.ts +370 -0
- package/templates/nextblock-template/lib/seo/robots-txt.ts +510 -0
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +177 -167
- package/templates/nextblock-template/lib/site-scripts/revisions.ts +71 -71
- package/templates/nextblock-template/lib/site-scripts/types.ts +46 -46
- package/templates/nextblock-template/lib/site-url.test.ts +89 -89
- package/templates/nextblock-template/lib/site-url.ts +102 -102
- package/templates/nextblock-template/lib/themes/buildThemeCss.ts +124 -124
- package/templates/nextblock-template/lib/themes/tokenColor.ts +31 -31
- package/templates/nextblock-template/lib/themes/tokens.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +0 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/proxy.ts +240 -20
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +102 -102
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/tools/lib/migrate-core.mjs +569 -569
- package/templates/nextblock-template/tools/update.mjs +1303 -1303
- package/tsconfig.base.json +3 -3
- 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<{
|
|
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
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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:
|
|
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(
|
|
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
|
+
}
|