create-nextblock 0.14.0 → 0.14.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 (242) hide show
  1. package/docker-template/.dockerignore +23 -23
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/libs/db/tsconfig.lib.json +3 -3
  8. package/libs/editor/tsconfig.lib.json +3 -3
  9. package/libs/ui/tsconfig.lib.json +3 -3
  10. package/libs/utils/tsconfig.json +3 -3
  11. package/package.json +1 -1
  12. package/project.json +19 -19
  13. package/templates/nextblock-template/.browserslistrc +11 -11
  14. package/templates/nextblock-template/.dockerignore +23 -23
  15. package/templates/nextblock-template/README.md +34 -34
  16. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  17. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  18. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  19. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  20. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
  21. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  22. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  23. package/templates/nextblock-template/app/actions/postActions.ts +146 -146
  24. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  25. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  26. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  27. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  28. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  29. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  30. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  31. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
  32. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5749 -5749
  33. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  34. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  35. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  36. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  37. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  38. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  39. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  40. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
  41. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  42. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  43. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  44. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  45. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  46. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +4 -2
  47. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
  48. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  49. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  50. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  51. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  52. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  53. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  54. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  55. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  56. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  57. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  58. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  59. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  60. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  62. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  63. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  64. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  65. package/templates/nextblock-template/app/cms/layout.tsx +73 -73
  66. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  67. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  68. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  69. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  70. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  71. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  72. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  73. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  74. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  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/promotions/PromotionsWorkspace.tsx +456 -456
  78. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  79. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  80. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
  81. package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
  82. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  83. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  84. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  85. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +688 -688
  86. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  87. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  88. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  89. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  90. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  91. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  92. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  93. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  94. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  95. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +23 -21
  96. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  97. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  98. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  99. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
  100. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  101. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  102. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +9 -7
  103. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  104. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  105. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  106. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  107. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  108. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  109. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  110. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  111. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  112. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  113. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  114. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  115. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  116. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  117. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  118. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  119. package/templates/nextblock-template/app/sitemap.ts +130 -130
  120. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  121. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  122. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  123. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  124. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  125. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  126. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  127. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  128. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  129. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  130. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  131. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  132. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  133. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  134. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  135. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
  136. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  137. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  138. package/templates/nextblock-template/components/form-message.tsx +32 -32
  139. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
  140. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
  141. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  142. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  143. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  144. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  145. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
  146. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  147. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  148. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  149. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  150. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  151. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  152. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  153. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  154. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  155. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  156. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
  157. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
  158. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
  159. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  160. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  161. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
  162. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
  163. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
  164. package/templates/nextblock-template/docs/README.md +39 -39
  165. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  166. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  167. package/templates/nextblock-template/index.d.ts +7 -7
  168. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  169. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  170. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  171. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  172. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  173. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  174. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  175. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
  176. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  177. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  178. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  179. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  180. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  181. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  182. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  183. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  184. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  185. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  186. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  187. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  188. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  189. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  190. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  191. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  192. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  193. package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
  194. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  195. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
  196. package/templates/nextblock-template/lib/media/youtube.ts +99 -99
  197. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  198. package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
  199. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  200. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  201. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  202. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  203. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  204. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  205. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  206. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  207. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  208. package/templates/nextblock-template/lib/search/types.ts +27 -27
  209. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  210. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  211. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  212. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +87 -87
  213. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  214. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  215. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  216. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  217. package/templates/nextblock-template/lib/site-url.ts +48 -48
  218. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  219. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  220. package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
  221. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  222. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  223. package/templates/nextblock-template/next-env.d.ts +1 -0
  224. package/templates/nextblock-template/package.json +6 -6
  225. package/templates/nextblock-template/postcss.config.js +6 -6
  226. package/templates/nextblock-template/scripts/backup.js +115 -115
  227. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  228. package/templates/nextblock-template/scripts/restore.js +385 -385
  229. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  230. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  231. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  232. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  233. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  234. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  235. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  236. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  237. package/templates/nextblock-template/tailwind.config.js +25 -25
  238. package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
  239. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  240. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  241. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
  242. package/tsconfig.base.json +3 -3
@@ -1,39 +1,39 @@
1
- import 'server-only';
2
- import { createHmac } from 'node:crypto';
3
-
4
- // Resolve the app's internal shared secrets WITHOUT requiring the operator to paste
5
- // random strings at deploy time. When an explicit env var is set it always wins;
6
- // otherwise the secret is derived deterministically from the always-present Supabase
7
- // service-role key. This is what lets a one-click Vercel deploy work with zero env
8
- // input while Draft Mode and on-demand revalidation still function out of the box.
9
- //
10
- // HMAC-SHA256 is one-way, so a leaked derived secret never exposes the service-role
11
- // key. The derived value is stable for a given Supabase project; rotating the
12
- // service-role key rotates these secrets too (set the explicit env var to pin one
13
- // independently — e.g. to configure a Supabase revalidation webhook with a fixed
14
- // token). `npm run setup` still writes explicit values locally, so dev is unchanged.
15
- //
16
- // CRON_SECRET is intentionally NOT derived here: Vercel Cron injects
17
- // `Authorization: Bearer ${process.env.CRON_SECRET}` read from the env at invocation,
18
- // so a derived value could never match it. The cron routes instead treat CRON_SECRET
19
- // as optional (enforced only when set) — see app/api/cron/*/route.ts.
20
-
21
- function deriveFromServiceRole(label: string): string {
22
- // Accept the Marketplace integration's SUPABASE_SECRET_KEY alias too, so derivation
23
- // still works on a one-click deploy that only injected the new key name.
24
- const master = (
25
- process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY
26
- )?.trim();
27
- if (!master) return '';
28
- return createHmac('sha256', master).update(`nextblock:${label}`).digest('hex');
29
- }
30
-
31
- /** Secret for the programmatic Draft Mode preview entry (`/api/draft?secret=`). */
32
- export function resolveDraftModeSecret(): string {
33
- return process.env.DRAFT_MODE_SECRET?.trim() || deriveFromServiceRole('draft-mode');
34
- }
35
-
36
- /** Shared secret a Supabase webhook sends to `/api/revalidate` (`x-revalidate-secret`). */
37
- export function resolveRevalidateSecret(): string {
38
- return process.env.REVALIDATE_SECRET_TOKEN?.trim() || deriveFromServiceRole('revalidate');
39
- }
1
+ import 'server-only';
2
+ import { createHmac } from 'node:crypto';
3
+
4
+ // Resolve the app's internal shared secrets WITHOUT requiring the operator to paste
5
+ // random strings at deploy time. When an explicit env var is set it always wins;
6
+ // otherwise the secret is derived deterministically from the always-present Supabase
7
+ // service-role key. This is what lets a one-click Vercel deploy work with zero env
8
+ // input while Draft Mode and on-demand revalidation still function out of the box.
9
+ //
10
+ // HMAC-SHA256 is one-way, so a leaked derived secret never exposes the service-role
11
+ // key. The derived value is stable for a given Supabase project; rotating the
12
+ // service-role key rotates these secrets too (set the explicit env var to pin one
13
+ // independently — e.g. to configure a Supabase revalidation webhook with a fixed
14
+ // token). `npm run setup` still writes explicit values locally, so dev is unchanged.
15
+ //
16
+ // CRON_SECRET is intentionally NOT derived here: Vercel Cron injects
17
+ // `Authorization: Bearer ${process.env.CRON_SECRET}` read from the env at invocation,
18
+ // so a derived value could never match it. The cron routes instead treat CRON_SECRET
19
+ // as optional (enforced only when set) — see app/api/cron/*/route.ts.
20
+
21
+ function deriveFromServiceRole(label: string): string {
22
+ // Accept the Marketplace integration's SUPABASE_SECRET_KEY alias too, so derivation
23
+ // still works on a one-click deploy that only injected the new key name.
24
+ const master = (
25
+ process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SECRET_KEY
26
+ )?.trim();
27
+ if (!master) return '';
28
+ return createHmac('sha256', master).update(`nextblock:${label}`).digest('hex');
29
+ }
30
+
31
+ /** Secret for the programmatic Draft Mode preview entry (`/api/draft?secret=`). */
32
+ export function resolveDraftModeSecret(): string {
33
+ return process.env.DRAFT_MODE_SECRET?.trim() || deriveFromServiceRole('draft-mode');
34
+ }
35
+
36
+ /** Shared secret a Supabase webhook sends to `/api/revalidate` (`x-revalidate-secret`). */
37
+ export function resolveRevalidateSecret(): string {
38
+ return process.env.REVALIDATE_SECRET_TOKEN?.trim() || deriveFromServiceRole('revalidate');
39
+ }
@@ -1,47 +1,47 @@
1
- // Server-only cookie helpers for the 2FA / trusted-device flow.
2
- // All three cookies are HttpOnly so they are never readable from JS.
3
- import { cookies } from 'next/headers';
4
-
5
- /** Long-lived "remember this device" token (raw token; its SHA-256 is in the DB). */
6
- export const TRUSTED_DEVICE_COOKIE = 'nb_trusted_device';
7
- /** Signed marker proving the email second factor was satisfied this session. */
8
- export const TWO_FACTOR_COOKIE = 'nb_2fa_verified';
9
- /** Short-lived flag carrying the "remember me" checkbox from login to post-2FA. */
10
- export const REMEMBER_INTENT_COOKIE = 'nb_remember_intent';
11
-
12
- const isProd = process.env.NODE_ENV === 'production';
13
-
14
- export async function setSecureCookie(
15
- name: string,
16
- value: string,
17
- maxAgeSeconds: number,
18
- ): Promise<void> {
19
- const store = await cookies();
20
- store.set({
21
- name,
22
- value,
23
- httpOnly: true,
24
- secure: isProd,
25
- sameSite: 'lax',
26
- path: '/',
27
- maxAge: maxAgeSeconds,
28
- });
29
- }
30
-
31
- export async function getCookieValue(name: string): Promise<string | null> {
32
- const store = await cookies();
33
- return store.get(name)?.value ?? null;
34
- }
35
-
36
- export async function clearCookie(name: string): Promise<void> {
37
- const store = await cookies();
38
- store.set({
39
- name,
40
- value: '',
41
- httpOnly: true,
42
- secure: isProd,
43
- sameSite: 'lax',
44
- path: '/',
45
- maxAge: 0,
46
- });
47
- }
1
+ // Server-only cookie helpers for the 2FA / trusted-device flow.
2
+ // All three cookies are HttpOnly so they are never readable from JS.
3
+ import { cookies } from 'next/headers';
4
+
5
+ /** Long-lived "remember this device" token (raw token; its SHA-256 is in the DB). */
6
+ export const TRUSTED_DEVICE_COOKIE = 'nb_trusted_device';
7
+ /** Signed marker proving the email second factor was satisfied this session. */
8
+ export const TWO_FACTOR_COOKIE = 'nb_2fa_verified';
9
+ /** Short-lived flag carrying the "remember me" checkbox from login to post-2FA. */
10
+ export const REMEMBER_INTENT_COOKIE = 'nb_remember_intent';
11
+
12
+ const isProd = process.env.NODE_ENV === 'production';
13
+
14
+ export async function setSecureCookie(
15
+ name: string,
16
+ value: string,
17
+ maxAgeSeconds: number,
18
+ ): Promise<void> {
19
+ const store = await cookies();
20
+ store.set({
21
+ name,
22
+ value,
23
+ httpOnly: true,
24
+ secure: isProd,
25
+ sameSite: 'lax',
26
+ path: '/',
27
+ maxAge: maxAgeSeconds,
28
+ });
29
+ }
30
+
31
+ export async function getCookieValue(name: string): Promise<string | null> {
32
+ const store = await cookies();
33
+ return store.get(name)?.value ?? null;
34
+ }
35
+
36
+ export async function clearCookie(name: string): Promise<void> {
37
+ const store = await cookies();
38
+ store.set({
39
+ name,
40
+ value: '',
41
+ httpOnly: true,
42
+ secure: isProd,
43
+ sameSite: 'lax',
44
+ path: '/',
45
+ maxAge: 0,
46
+ });
47
+ }
@@ -1,45 +1,45 @@
1
- // Server-only crypto helpers for 2FA tokens, trusted-device hashing, and the
2
- // signed "second factor satisfied" session marker. Uses Node's crypto module.
3
- import crypto from 'node:crypto';
4
-
5
- /** SHA-256 hex digest. Used to store device/code hashes (never the raw value). */
6
- export function sha256Hex(input: string): string {
7
- return crypto.createHash('sha256').update(input).digest('hex');
8
- }
9
-
10
- /** Cryptographically random URL-safe token (default 32 bytes -> 43 chars). */
11
- export function randomToken(bytes = 32): string {
12
- return crypto.randomBytes(bytes).toString('base64url');
13
- }
14
-
15
- /** Random fixed-length numeric code, e.g. a 6-digit email 2FA code. */
16
- export function generateNumericCode(digits = 6): string {
17
- const max = 10 ** digits;
18
- return crypto.randomInt(0, max).toString().padStart(digits, '0');
19
- }
20
-
21
- function getSecret(): string {
22
- // Dedicated secret if provided, otherwise fall back to the service-role key
23
- // (server-only, never shipped to the client).
24
- const secret =
25
- process.env.NB_2FA_SECRET ||
26
- process.env.SUPABASE_SERVICE_ROLE_KEY ||
27
- process.env.SUPABASE_SECRET_KEY;
28
- if (!secret) {
29
- throw new Error('Missing NB_2FA_SECRET / SUPABASE_SERVICE_ROLE_KEY for 2FA signing.');
30
- }
31
- return secret;
32
- }
33
-
34
- /** HMAC-SHA256 signature (base64url) of an arbitrary payload string. */
35
- export function hmacSign(payload: string): string {
36
- return crypto.createHmac('sha256', getSecret()).update(payload).digest('base64url');
37
- }
38
-
39
- /** Constant-time string comparison that never throws on length mismatch. */
40
- export function safeEqual(a: string, b: string): boolean {
41
- const ab = Buffer.from(a);
42
- const bb = Buffer.from(b);
43
- if (ab.length !== bb.length) return false;
44
- return crypto.timingSafeEqual(ab, bb);
45
- }
1
+ // Server-only crypto helpers for 2FA tokens, trusted-device hashing, and the
2
+ // signed "second factor satisfied" session marker. Uses Node's crypto module.
3
+ import crypto from 'node:crypto';
4
+
5
+ /** SHA-256 hex digest. Used to store device/code hashes (never the raw value). */
6
+ export function sha256Hex(input: string): string {
7
+ return crypto.createHash('sha256').update(input).digest('hex');
8
+ }
9
+
10
+ /** Cryptographically random URL-safe token (default 32 bytes -> 43 chars). */
11
+ export function randomToken(bytes = 32): string {
12
+ return crypto.randomBytes(bytes).toString('base64url');
13
+ }
14
+
15
+ /** Random fixed-length numeric code, e.g. a 6-digit email 2FA code. */
16
+ export function generateNumericCode(digits = 6): string {
17
+ const max = 10 ** digits;
18
+ return crypto.randomInt(0, max).toString().padStart(digits, '0');
19
+ }
20
+
21
+ function getSecret(): string {
22
+ // Dedicated secret if provided, otherwise fall back to the service-role key
23
+ // (server-only, never shipped to the client).
24
+ const secret =
25
+ process.env.NB_2FA_SECRET ||
26
+ process.env.SUPABASE_SERVICE_ROLE_KEY ||
27
+ process.env.SUPABASE_SECRET_KEY;
28
+ if (!secret) {
29
+ throw new Error('Missing NB_2FA_SECRET / SUPABASE_SERVICE_ROLE_KEY for 2FA signing.');
30
+ }
31
+ return secret;
32
+ }
33
+
34
+ /** HMAC-SHA256 signature (base64url) of an arbitrary payload string. */
35
+ export function hmacSign(payload: string): string {
36
+ return crypto.createHmac('sha256', getSecret()).update(payload).digest('base64url');
37
+ }
38
+
39
+ /** Constant-time string comparison that never throws on length mismatch. */
40
+ export function safeEqual(a: string, b: string): boolean {
41
+ const ab = Buffer.from(a);
42
+ const bb = Buffer.from(b);
43
+ if (ab.length !== bb.length) return false;
44
+ return crypto.timingSafeEqual(ab, bb);
45
+ }
@@ -1,92 +1,92 @@
1
- // Server-only trusted-device ("Remember this device") helpers.
2
- //
3
- // Trust is server-validated: the cookie holds only a random token, and a bypass
4
- // is honoured ONLY when a non-expired user_trusted_devices row matches its
5
- // SHA-256. Deleting the row instantly revokes trust, regardless of the cookie.
6
- import { createClient, getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
7
- import { getSecuritySettings } from '../privacy/settings';
8
- import { randomToken, sha256Hex } from './crypto';
9
- import {
10
- TRUSTED_DEVICE_COOKIE,
11
- clearCookie,
12
- getCookieValue,
13
- setSecureCookie,
14
- } from './cookies';
15
-
16
- export interface TrustedDeviceRow {
17
- id: string;
18
- browser_metadata: string | null;
19
- created_at: string;
20
- expires_at: string;
21
- }
22
-
23
- /** Mint a trusted-device token, persist its hash, and set the cookie. */
24
- export async function issueTrustedDevice(
25
- userId: string,
26
- browserMetadata: string | null,
27
- ): Promise<void> {
28
- const { trusted_device_days } = await getSecuritySettings();
29
- const maxAgeSeconds = trusted_device_days * 24 * 60 * 60;
30
- const token = randomToken(32);
31
- const expiresAt = new Date(Date.now() + maxAgeSeconds * 1000).toISOString();
32
-
33
- const svc = getServiceRoleSupabaseClient();
34
- const { error } = await svc.from('user_trusted_devices').insert({
35
- user_id: userId,
36
- device_hash: sha256Hex(token),
37
- browser_metadata: browserMetadata ? browserMetadata.slice(0, 500) : null,
38
- expires_at: expiresAt,
39
- });
40
- if (error) {
41
- console.error('Failed to persist trusted device:', error.message);
42
- return; // Non-fatal: the user simply isn't remembered.
43
- }
44
- await setSecureCookie(TRUSTED_DEVICE_COOKIE, token, maxAgeSeconds);
45
- }
46
-
47
- /** True when the current request carries a cookie matching a live trusted row. */
48
- export async function hasValidTrustedDevice(userId: string): Promise<boolean> {
49
- const token = await getCookieValue(TRUSTED_DEVICE_COOKIE);
50
- if (!token) return false;
51
- const svc = getServiceRoleSupabaseClient();
52
- const { data } = await svc
53
- .from('user_trusted_devices')
54
- .select('id')
55
- .eq('user_id', userId)
56
- .eq('device_hash', sha256Hex(token))
57
- .gt('expires_at', new Date().toISOString())
58
- .maybeSingle();
59
- return Boolean(data);
60
- }
61
-
62
- /** List the signed-in user's trusted devices (RLS scopes to their own rows). */
63
- export async function listTrustedDevices(userId: string): Promise<TrustedDeviceRow[]> {
64
- const supabase = createClient();
65
- const { data } = await supabase
66
- .from('user_trusted_devices')
67
- .select('id, browser_metadata, created_at, expires_at')
68
- .eq('user_id', userId)
69
- .order('created_at', { ascending: false });
70
- return data ?? [];
71
- }
72
-
73
- /** Revoke one trusted device the user owns (RLS prevents touching others'). */
74
- export async function revokeTrustedDevice(userId: string, id: string): Promise<void> {
75
- const supabase = createClient();
76
- const { error } = await supabase
77
- .from('user_trusted_devices')
78
- .delete()
79
- .eq('id', id)
80
- .eq('user_id', userId);
81
- if (error) {
82
- console.error('Failed to revoke trusted device:', error.message);
83
- throw new Error('Failed to revoke device.');
84
- }
85
- }
86
-
87
- /** Drop all of a user's trusted devices and the local cookie (e.g. on MFA disable). */
88
- export async function revokeAllTrustedDevices(userId: string): Promise<void> {
89
- const svc = getServiceRoleSupabaseClient();
90
- await svc.from('user_trusted_devices').delete().eq('user_id', userId);
91
- await clearCookie(TRUSTED_DEVICE_COOKIE);
92
- }
1
+ // Server-only trusted-device ("Remember this device") helpers.
2
+ //
3
+ // Trust is server-validated: the cookie holds only a random token, and a bypass
4
+ // is honoured ONLY when a non-expired user_trusted_devices row matches its
5
+ // SHA-256. Deleting the row instantly revokes trust, regardless of the cookie.
6
+ import { createClient, getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
7
+ import { getSecuritySettings } from '../privacy/settings';
8
+ import { randomToken, sha256Hex } from './crypto';
9
+ import {
10
+ TRUSTED_DEVICE_COOKIE,
11
+ clearCookie,
12
+ getCookieValue,
13
+ setSecureCookie,
14
+ } from './cookies';
15
+
16
+ export interface TrustedDeviceRow {
17
+ id: string;
18
+ browser_metadata: string | null;
19
+ created_at: string;
20
+ expires_at: string;
21
+ }
22
+
23
+ /** Mint a trusted-device token, persist its hash, and set the cookie. */
24
+ export async function issueTrustedDevice(
25
+ userId: string,
26
+ browserMetadata: string | null,
27
+ ): Promise<void> {
28
+ const { trusted_device_days } = await getSecuritySettings();
29
+ const maxAgeSeconds = trusted_device_days * 24 * 60 * 60;
30
+ const token = randomToken(32);
31
+ const expiresAt = new Date(Date.now() + maxAgeSeconds * 1000).toISOString();
32
+
33
+ const svc = getServiceRoleSupabaseClient();
34
+ const { error } = await svc.from('user_trusted_devices').insert({
35
+ user_id: userId,
36
+ device_hash: sha256Hex(token),
37
+ browser_metadata: browserMetadata ? browserMetadata.slice(0, 500) : null,
38
+ expires_at: expiresAt,
39
+ });
40
+ if (error) {
41
+ console.error('Failed to persist trusted device:', error.message);
42
+ return; // Non-fatal: the user simply isn't remembered.
43
+ }
44
+ await setSecureCookie(TRUSTED_DEVICE_COOKIE, token, maxAgeSeconds);
45
+ }
46
+
47
+ /** True when the current request carries a cookie matching a live trusted row. */
48
+ export async function hasValidTrustedDevice(userId: string): Promise<boolean> {
49
+ const token = await getCookieValue(TRUSTED_DEVICE_COOKIE);
50
+ if (!token) return false;
51
+ const svc = getServiceRoleSupabaseClient();
52
+ const { data } = await svc
53
+ .from('user_trusted_devices')
54
+ .select('id')
55
+ .eq('user_id', userId)
56
+ .eq('device_hash', sha256Hex(token))
57
+ .gt('expires_at', new Date().toISOString())
58
+ .maybeSingle();
59
+ return Boolean(data);
60
+ }
61
+
62
+ /** List the signed-in user's trusted devices (RLS scopes to their own rows). */
63
+ export async function listTrustedDevices(userId: string): Promise<TrustedDeviceRow[]> {
64
+ const supabase = createClient();
65
+ const { data } = await supabase
66
+ .from('user_trusted_devices')
67
+ .select('id, browser_metadata, created_at, expires_at')
68
+ .eq('user_id', userId)
69
+ .order('created_at', { ascending: false });
70
+ return data ?? [];
71
+ }
72
+
73
+ /** Revoke one trusted device the user owns (RLS prevents touching others'). */
74
+ export async function revokeTrustedDevice(userId: string, id: string): Promise<void> {
75
+ const supabase = createClient();
76
+ const { error } = await supabase
77
+ .from('user_trusted_devices')
78
+ .delete()
79
+ .eq('id', id)
80
+ .eq('user_id', userId);
81
+ if (error) {
82
+ console.error('Failed to revoke trusted device:', error.message);
83
+ throw new Error('Failed to revoke device.');
84
+ }
85
+ }
86
+
87
+ /** Drop all of a user's trusted devices and the local cookie (e.g. on MFA disable). */
88
+ export async function revokeAllTrustedDevices(userId: string): Promise<void> {
89
+ const svc = getServiceRoleSupabaseClient();
90
+ await svc.from('user_trusted_devices').delete().eq('user_id', userId);
91
+ await clearCookie(TRUSTED_DEVICE_COOKIE);
92
+ }
@@ -1,46 +1,46 @@
1
- import type { Database } from '@nextblock-cms/db';
2
-
3
- type AuthProfile = Pick<Database['public']['Tables']['profiles']['Row'], 'role' | 'full_name'> | null;
4
-
5
- const DASHBOARD_PATH = '/cms/dashboard';
6
- const HOME_PATH = '/';
7
- const PROFILE_PATH = '/profile';
8
- const PASSWORD_RESET_PATH = '/profile/password';
9
-
10
- function isSafeInternalPath(path?: string | null) {
11
- return Boolean(path && path.startsWith('/') && !path.startsWith('//'));
12
- }
13
-
14
- function isDashboardPath(path: string) {
15
- return path === DASHBOARD_PATH || path.startsWith(`${DASHBOARD_PATH}/`);
16
- }
17
-
18
- export function isCustomerProfileComplete(profile: AuthProfile) {
19
- return Boolean(profile?.full_name?.trim());
20
- }
21
-
22
- export function resolvePostAuthRedirect(
23
- profile: AuthProfile,
24
- requestedPath?: string | null
25
- ) {
26
- const safePath = isSafeInternalPath(requestedPath) ? requestedPath : null;
27
- const role = profile?.role ?? null;
28
-
29
- if (safePath === PASSWORD_RESET_PATH) {
30
- return PASSWORD_RESET_PATH;
31
- }
32
-
33
- if (role === 'ADMIN' || role === 'WRITER') {
34
- return safePath ?? DASHBOARD_PATH;
35
- }
36
-
37
- if (!isCustomerProfileComplete(profile)) {
38
- return PROFILE_PATH;
39
- }
40
-
41
- if (safePath && !isDashboardPath(safePath)) {
42
- return safePath;
43
- }
44
-
45
- return HOME_PATH;
46
- }
1
+ import type { Database } from '@nextblock-cms/db';
2
+
3
+ type AuthProfile = Pick<Database['public']['Tables']['profiles']['Row'], 'role' | 'full_name'> | null;
4
+
5
+ const DASHBOARD_PATH = '/cms/dashboard';
6
+ const HOME_PATH = '/';
7
+ const PROFILE_PATH = '/profile';
8
+ const PASSWORD_RESET_PATH = '/profile/password';
9
+
10
+ function isSafeInternalPath(path?: string | null) {
11
+ return Boolean(path && path.startsWith('/') && !path.startsWith('//'));
12
+ }
13
+
14
+ function isDashboardPath(path: string) {
15
+ return path === DASHBOARD_PATH || path.startsWith(`${DASHBOARD_PATH}/`);
16
+ }
17
+
18
+ export function isCustomerProfileComplete(profile: AuthProfile) {
19
+ return Boolean(profile?.full_name?.trim());
20
+ }
21
+
22
+ export function resolvePostAuthRedirect(
23
+ profile: AuthProfile,
24
+ requestedPath?: string | null
25
+ ) {
26
+ const safePath = isSafeInternalPath(requestedPath) ? requestedPath : null;
27
+ const role = profile?.role ?? null;
28
+
29
+ if (safePath === PASSWORD_RESET_PATH) {
30
+ return PASSWORD_RESET_PATH;
31
+ }
32
+
33
+ if (role === 'ADMIN' || role === 'WRITER') {
34
+ return safePath ?? DASHBOARD_PATH;
35
+ }
36
+
37
+ if (!isCustomerProfileComplete(profile)) {
38
+ return PROFILE_PATH;
39
+ }
40
+
41
+ if (safePath && !isDashboardPath(safePath)) {
42
+ return safePath;
43
+ }
44
+
45
+ return HOME_PATH;
46
+ }
@@ -1,13 +1,13 @@
1
- # NextBlock™ Block System
2
-
3
- The built-in block registry for the app lives in this directory. The current
4
- source of truth is `blockRegistry.ts`, which defines block types, schemas,
5
- defaults, and editor or renderer wiring for the CMS.
6
-
7
- ## Read This First
8
-
9
- - [CMS and Editor](../../../../docs/03-CMS-AND-EDITOR.md)
10
- - [Block SDK and Extensibility](../../../../docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md)
11
- - [Custom Blocks (Data-Driven CRUD)](../../../../docs/10-CUSTOM-BLOCKS.md)
12
-
13
- Use those docs instead of maintaining a second long-form block tutorial here.
1
+ # NextBlock™ Block System
2
+
3
+ The built-in block registry for the app lives in this directory. The current
4
+ source of truth is `blockRegistry.ts`, which defines block types, schemas,
5
+ defaults, and editor or renderer wiring for the CMS.
6
+
7
+ ## Read This First
8
+
9
+ - [CMS and Editor](../../../../docs/03-CMS-AND-EDITOR.md)
10
+ - [Block SDK and Extensibility](../../../../docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md)
11
+ - [Custom Blocks (Data-Driven CRUD)](../../../../docs/10-CUSTOM-BLOCKS.md)
12
+
13
+ Use those docs instead of maintaining a second long-form block tutorial here.