create-nextblock 0.13.10 → 0.13.11

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 (255) 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/productGridActions.ts +40 -40
  25. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  26. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  27. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  28. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  29. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  30. package/templates/nextblock-template/app/api/cms/ecommerce/product-picker/route.ts +151 -151
  31. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  32. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  33. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5749 -5609
  35. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  36. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  37. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  38. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  39. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  40. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  41. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  42. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
  43. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  44. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  45. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  46. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  47. package/templates/nextblock-template/app/cms/blocks/components/MultiEntityPicker.tsx +263 -263
  48. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  49. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
  50. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  51. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  52. package/templates/nextblock-template/app/cms/components/EcommerceActiveContext.tsx +27 -27
  53. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  54. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  55. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  56. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  57. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  58. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  59. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  60. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  61. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  62. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  63. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  64. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  65. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  66. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  67. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  68. package/templates/nextblock-template/app/cms/interactions/EmailRecipientsInput.tsx +189 -189
  69. package/templates/nextblock-template/app/cms/layout.tsx +73 -73
  70. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  71. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  72. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  73. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  74. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  75. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  76. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  77. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  78. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  79. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  80. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  81. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  82. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  83. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  84. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
  85. package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
  86. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  87. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  88. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  89. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +497 -497
  90. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  91. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  92. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  93. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  94. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  95. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  96. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageDetectionPanel.tsx +188 -188
  97. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  98. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  99. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  100. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -20
  101. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  102. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  103. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  104. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
  105. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  106. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  107. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  108. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  109. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  110. package/templates/nextblock-template/app/cms/users/components/CreateUserForm.tsx +217 -217
  111. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  112. package/templates/nextblock-template/app/cms/users/new/page.tsx +44 -44
  113. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  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/setup/SetupWizard.tsx +678 -678
  123. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  124. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  125. package/templates/nextblock-template/app/sitemap.ts +130 -130
  126. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  127. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  128. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  129. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  130. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  131. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  132. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  133. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  134. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  135. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  136. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  137. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  138. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  139. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  140. package/templates/nextblock-template/components/blocks/ProductGridClient.tsx +114 -114
  141. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  142. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +8 -1
  143. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
  144. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +29 -23
  145. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  146. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  147. package/templates/nextblock-template/components/form-message.tsx +32 -32
  148. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +69 -0
  149. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -0
  150. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  151. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  152. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  153. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  154. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -464
  155. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  156. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  157. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  158. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  159. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  160. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  161. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  162. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  163. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  164. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  165. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
  166. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
  167. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
  168. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  169. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  170. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
  171. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
  172. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
  173. package/templates/nextblock-template/docs/README.md +39 -39
  174. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  175. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  176. package/templates/nextblock-template/index.d.ts +7 -7
  177. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  178. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  179. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  180. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  181. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  182. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +78 -78
  183. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  184. package/templates/nextblock-template/lib/blocks/product-grid-data.ts +210 -210
  185. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  186. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
  187. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  188. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  189. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  190. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  191. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  192. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  193. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  194. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  195. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  196. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  197. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  198. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  199. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  200. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  201. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  202. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  203. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  204. package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
  205. package/templates/nextblock-template/lib/i18n/country-languages.ts +247 -247
  206. package/templates/nextblock-template/lib/i18n/detection.test.ts +197 -197
  207. package/templates/nextblock-template/lib/i18n/detection.ts +192 -192
  208. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  209. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
  210. package/templates/nextblock-template/lib/media/youtube.ts +81 -0
  211. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  212. package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
  213. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  214. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  215. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  216. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  217. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  218. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  219. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  220. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  221. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  222. package/templates/nextblock-template/lib/search/types.ts +27 -27
  223. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  224. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  225. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  226. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +87 -82
  227. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  228. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  229. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  230. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  231. package/templates/nextblock-template/lib/site-url.ts +48 -48
  232. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  233. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  234. package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
  235. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  236. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  237. package/templates/nextblock-template/package.json +1 -1
  238. package/templates/nextblock-template/postcss.config.js +6 -6
  239. package/templates/nextblock-template/scripts/backup.js +115 -115
  240. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  241. package/templates/nextblock-template/scripts/restore.js +385 -385
  242. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  243. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  244. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  245. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  246. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  247. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  248. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  249. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  250. package/templates/nextblock-template/tailwind.config.js +25 -25
  251. package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
  252. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  253. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  254. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
  255. 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
+ }