create-nextblock 0.12.9 → 0.12.12
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 +23 -23
- package/docker-template/.env.docker.example +69 -69
- package/docker-template/docker/db/init/99-jwt.sql +6 -6
- package/docker-template/docker/db/init/99-roles.sql +25 -25
- package/docker-template/docker/kong/kong.yml +112 -112
- package/docker-template/scripts/docker-setup.mjs +310 -310
- 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 +1 -1
- package/project.json +19 -19
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.dockerignore +23 -23
- package/templates/nextblock-template/README.md +34 -34
- 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/(auth-pages)/sign-up/SignUpForm.tsx +141 -141
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -91
- package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +123 -123
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
- package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- package/templates/nextblock-template/app/actions/interactions.test.ts +301 -301
- package/templates/nextblock-template/app/actions/interactions.ts +373 -373
- package/templates/nextblock-template/app/actions/postActions.ts +146 -146
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +964 -964
- package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5597 -5597
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
- package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
- 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/article/[slug]/PostClientContent.tsx +441 -441
- 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/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
- package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -244
- package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
- package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -1522
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
- package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +431 -391
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -408
- package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -51
- package/templates/nextblock-template/app/cms/layout.tsx +73 -73
- 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 +26 -26
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +149 -158
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -101
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
- package/templates/nextblock-template/app/cms/pages/page.tsx +18 -17
- package/templates/nextblock-template/app/cms/posts/page.tsx +19 -18
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- package/templates/nextblock-template/app/cms/products/page.tsx +39 -27
- 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/promotions/PromotionsWorkspace.tsx +456 -456
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
- package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
- package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
- package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -93
- package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -129
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +497 -497
- package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -410
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +246 -246
- package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -80
- 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/actions.ts +64 -64
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +181 -181
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
- package/templates/nextblock-template/app/cms/settings/google-analytics/actions.ts +60 -60
- package/templates/nextblock-template/app/cms/settings/google-analytics/components/GoogleAnalyticsForm.tsx +129 -129
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +16 -16
- 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/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -20
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
- package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
- package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +52 -52
- package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +148 -148
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/actions.ts +44 -44
- package/templates/nextblock-template/app/cms/settings/registration/components/RegistrationForm.tsx +65 -65
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/actions.ts +284 -284
- package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +522 -522
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- 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/actions.ts +222 -222
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +44 -44
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +253 -253
- package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -73
- 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-data.ts +37 -37
- package/templates/nextblock-template/app/profile/account-links.ts +22 -22
- package/templates/nextblock-template/app/profile/account-types.ts +11 -11
- package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
- package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -79
- package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -32
- package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
- package/templates/nextblock-template/app/profile/page.tsx +42 -42
- 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/setup/SetupWizard.tsx +678 -678
- package/templates/nextblock-template/app/setup/layout.tsx +13 -13
- package/templates/nextblock-template/app/setup/page.tsx +111 -111
- package/templates/nextblock-template/app/sitemap.ts +130 -130
- package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
- package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
- package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
- package/templates/nextblock-template/components/GitHubLoginButton.tsx +15 -15
- 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 +369 -369
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -419
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
- package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
- package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
- 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/ecommerceRendererLoaders.ts +23 -23
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
- package/templates/nextblock-template/components/blocks/types.ts +7 -7
- 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/privacy/ConsentBanner.tsx +170 -170
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -464
- package/templates/nextblock-template/components/submit-button.tsx +23 -23
- package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/AuthContext.tsx +23 -23
- package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
- package/templates/nextblock-template/context/language-rest-client.ts +32 -32
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
- package/templates/nextblock-template/docker/kong/kong.yml +112 -112
- package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
- package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -1319
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
- package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
- package/templates/nextblock-template/docs/README.md +39 -39
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12505 -12505
- 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/app-secrets.ts +39 -39
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
- package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
- package/templates/nextblock-template/lib/auth/twoFactor.ts +208 -208
- package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +6 -6
- package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -18
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +100 -77
- package/templates/nextblock-template/lib/cms-transfer/csv.ts +403 -399
- package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- package/templates/nextblock-template/lib/config/email-settings.ts +217 -217
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
- package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -256
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
- package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
- package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
- package/templates/nextblock-template/lib/email/branding.ts +76 -76
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
- package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
- package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
- package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
- package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
- package/templates/nextblock-template/lib/privacy/types.ts +69 -69
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
- package/templates/nextblock-template/lib/promotions/server.ts +741 -741
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
- package/templates/nextblock-template/lib/search/server.ts +585 -585
- package/templates/nextblock-template/lib/search/types.ts +27 -27
- package/templates/nextblock-template/lib/setup/actions.ts +460 -460
- package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -37
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/site-url.ts +48 -48
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/postcss.config.js +6 -6
- package/templates/nextblock-template/scripts/backup.js +115 -115
- package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/restore.js +385 -385
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/tailwind.config.js +25 -25
- package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
- package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
- package/tsconfig.base.json +3 -3
- package/templates/nextblock-template/tsconfig.tsbuildinfo +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, type DependencyList } from 'react';
|
|
1
|
+
import { useEffect, type DependencyList } from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Hook to handle keyboard shortcuts.
|
|
@@ -8,26 +8,26 @@ import { useEffect, type DependencyList } from 'react';
|
|
|
8
8
|
* @param callback The function to call when the key combination is pressed
|
|
9
9
|
* @param deps Dependencies array for the effect
|
|
10
10
|
*/
|
|
11
|
-
export function useHotkeys(
|
|
12
|
-
key: string,
|
|
13
|
-
callback: (event: KeyboardEvent) => void,
|
|
14
|
-
deps: DependencyList = [],
|
|
15
|
-
) {
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
const normalizedShortcut = key.toLowerCase();
|
|
18
|
-
|
|
19
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
20
|
-
const isCtrl = event.ctrlKey || event.metaKey; // cmd on mac, ctrl on windows
|
|
21
|
-
const keyLower = typeof event.key === 'string' ? event.key.toLowerCase() : '';
|
|
22
|
-
const isSaveKey = keyLower === 's' || event.code === 'KeyS';
|
|
23
|
-
|
|
24
|
-
// Check for ctrl+s / cmd+s
|
|
25
|
-
if ((normalizedShortcut === 'ctrl+s' || normalizedShortcut === 'meta+s') && isCtrl && isSaveKey) {
|
|
26
|
-
event.preventDefault();
|
|
27
|
-
callback(event);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
11
|
+
export function useHotkeys(
|
|
12
|
+
key: string,
|
|
13
|
+
callback: (event: KeyboardEvent) => void,
|
|
14
|
+
deps: DependencyList = [],
|
|
15
|
+
) {
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const normalizedShortcut = key.toLowerCase();
|
|
18
|
+
|
|
19
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
20
|
+
const isCtrl = event.ctrlKey || event.metaKey; // cmd on mac, ctrl on windows
|
|
21
|
+
const keyLower = typeof event.key === 'string' ? event.key.toLowerCase() : '';
|
|
22
|
+
const isSaveKey = keyLower === 's' || event.code === 'KeyS';
|
|
23
|
+
|
|
24
|
+
// Check for ctrl+s / cmd+s
|
|
25
|
+
if ((normalizedShortcut === 'ctrl+s' || normalizedShortcut === 'meta+s') && isCtrl && isSaveKey) {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
callback(event);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
31
|
window.addEventListener('keydown', handleKeyDown);
|
|
32
32
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
33
33
|
}, [key, ...deps]); // callback should be stable or included in deps if handled by caller
|
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
-
import { useLanguage } from '../context/LanguageContext';
|
|
5
|
-
import type {
|
|
6
|
-
GlobalSearchFilter,
|
|
7
|
-
GlobalSearchResponse,
|
|
8
|
-
GlobalSearchResult,
|
|
9
|
-
} from '../lib/search/types';
|
|
10
|
-
|
|
11
|
-
type SearchStatus = 'idle' | 'loading' | 'success' | 'error';
|
|
12
|
-
|
|
13
|
-
export function useGlobalSearch({
|
|
14
|
-
initialQuery = '',
|
|
15
|
-
initialFilter = 'all',
|
|
16
|
-
limit = 12,
|
|
17
|
-
}: {
|
|
18
|
-
initialQuery?: string;
|
|
19
|
-
initialFilter?: GlobalSearchFilter;
|
|
20
|
-
limit?: number;
|
|
21
|
-
} = {}) {
|
|
22
|
-
const { currentLocale } = useLanguage();
|
|
23
|
-
const [query, setQuery] = useState(initialQuery);
|
|
24
|
-
const [filter, setFilter] = useState<GlobalSearchFilter>(initialFilter);
|
|
25
|
-
const [results, setResults] = useState<GlobalSearchResult[]>([]);
|
|
26
|
-
const [counts, setCounts] = useState<GlobalSearchResponse['counts']>({
|
|
27
|
-
all: 0,
|
|
28
|
-
page: 0,
|
|
29
|
-
post: 0,
|
|
30
|
-
product: 0,
|
|
31
|
-
});
|
|
32
|
-
const [status, setStatus] = useState<SearchStatus>('idle');
|
|
33
|
-
|
|
34
|
-
const normalizedQuery = useMemo(() => query.replace(/\s+/g, ' ').trim(), [query]);
|
|
35
|
-
const canSearch = normalizedQuery.length >= 2;
|
|
36
|
-
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
if (!canSearch) {
|
|
39
|
-
setResults([]);
|
|
40
|
-
setCounts({ all: 0, page: 0, post: 0, product: 0 });
|
|
41
|
-
setStatus('idle');
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const controller = new AbortController();
|
|
46
|
-
const timeoutId = window.setTimeout(async () => {
|
|
47
|
-
setStatus('loading');
|
|
48
|
-
|
|
49
|
-
const params = new URLSearchParams({
|
|
50
|
-
q: normalizedQuery,
|
|
51
|
-
type: filter,
|
|
52
|
-
limit: String(limit),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
if (currentLocale) {
|
|
56
|
-
params.set('locale', currentLocale);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const response = await fetch(`/api/search?${params.toString()}`, {
|
|
61
|
-
signal: controller.signal,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
if (!response.ok) {
|
|
65
|
-
throw new Error(`Search request failed with ${response.status}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const payload = (await response.json()) as GlobalSearchResponse;
|
|
69
|
-
setResults(payload.results);
|
|
70
|
-
setCounts(payload.counts);
|
|
71
|
-
setStatus('success');
|
|
72
|
-
} catch (error) {
|
|
73
|
-
if ((error as Error).name === 'AbortError') {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
console.error('Global search request failed:', error);
|
|
78
|
-
setResults([]);
|
|
79
|
-
setCounts({ all: 0, page: 0, post: 0, product: 0 });
|
|
80
|
-
setStatus('error');
|
|
81
|
-
}
|
|
82
|
-
}, 180);
|
|
83
|
-
|
|
84
|
-
return () => {
|
|
85
|
-
controller.abort();
|
|
86
|
-
window.clearTimeout(timeoutId);
|
|
87
|
-
};
|
|
88
|
-
}, [canSearch, currentLocale, filter, limit, normalizedQuery]);
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
query,
|
|
92
|
-
setQuery,
|
|
93
|
-
filter,
|
|
94
|
-
setFilter,
|
|
95
|
-
results,
|
|
96
|
-
counts,
|
|
97
|
-
status,
|
|
98
|
-
canSearch,
|
|
99
|
-
isLoading: status === 'loading',
|
|
100
|
-
};
|
|
101
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { useLanguage } from '../context/LanguageContext';
|
|
5
|
+
import type {
|
|
6
|
+
GlobalSearchFilter,
|
|
7
|
+
GlobalSearchResponse,
|
|
8
|
+
GlobalSearchResult,
|
|
9
|
+
} from '../lib/search/types';
|
|
10
|
+
|
|
11
|
+
type SearchStatus = 'idle' | 'loading' | 'success' | 'error';
|
|
12
|
+
|
|
13
|
+
export function useGlobalSearch({
|
|
14
|
+
initialQuery = '',
|
|
15
|
+
initialFilter = 'all',
|
|
16
|
+
limit = 12,
|
|
17
|
+
}: {
|
|
18
|
+
initialQuery?: string;
|
|
19
|
+
initialFilter?: GlobalSearchFilter;
|
|
20
|
+
limit?: number;
|
|
21
|
+
} = {}) {
|
|
22
|
+
const { currentLocale } = useLanguage();
|
|
23
|
+
const [query, setQuery] = useState(initialQuery);
|
|
24
|
+
const [filter, setFilter] = useState<GlobalSearchFilter>(initialFilter);
|
|
25
|
+
const [results, setResults] = useState<GlobalSearchResult[]>([]);
|
|
26
|
+
const [counts, setCounts] = useState<GlobalSearchResponse['counts']>({
|
|
27
|
+
all: 0,
|
|
28
|
+
page: 0,
|
|
29
|
+
post: 0,
|
|
30
|
+
product: 0,
|
|
31
|
+
});
|
|
32
|
+
const [status, setStatus] = useState<SearchStatus>('idle');
|
|
33
|
+
|
|
34
|
+
const normalizedQuery = useMemo(() => query.replace(/\s+/g, ' ').trim(), [query]);
|
|
35
|
+
const canSearch = normalizedQuery.length >= 2;
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!canSearch) {
|
|
39
|
+
setResults([]);
|
|
40
|
+
setCounts({ all: 0, page: 0, post: 0, product: 0 });
|
|
41
|
+
setStatus('idle');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timeoutId = window.setTimeout(async () => {
|
|
47
|
+
setStatus('loading');
|
|
48
|
+
|
|
49
|
+
const params = new URLSearchParams({
|
|
50
|
+
q: normalizedQuery,
|
|
51
|
+
type: filter,
|
|
52
|
+
limit: String(limit),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (currentLocale) {
|
|
56
|
+
params.set('locale', currentLocale);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(`/api/search?${params.toString()}`, {
|
|
61
|
+
signal: controller.signal,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(`Search request failed with ${response.status}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const payload = (await response.json()) as GlobalSearchResponse;
|
|
69
|
+
setResults(payload.results);
|
|
70
|
+
setCounts(payload.counts);
|
|
71
|
+
setStatus('success');
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if ((error as Error).name === 'AbortError') {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.error('Global search request failed:', error);
|
|
78
|
+
setResults([]);
|
|
79
|
+
setCounts({ all: 0, page: 0, post: 0, product: 0 });
|
|
80
|
+
setStatus('error');
|
|
81
|
+
}
|
|
82
|
+
}, 180);
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
controller.abort();
|
|
86
|
+
window.clearTimeout(timeoutId);
|
|
87
|
+
};
|
|
88
|
+
}, [canSearch, currentLocale, filter, limit, normalizedQuery]);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
query,
|
|
92
|
+
setQuery,
|
|
93
|
+
filter,
|
|
94
|
+
setFilter,
|
|
95
|
+
results,
|
|
96
|
+
counts,
|
|
97
|
+
status,
|
|
98
|
+
canSearch,
|
|
99
|
+
isLoading: status === 'loading',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
declare module '*.svg' {
|
|
2
|
-
const content: any;
|
|
3
|
-
export const ReactComponent: any;
|
|
4
|
-
export default content;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
declare module 'uuid';
|
|
1
|
+
declare module '*.svg' {
|
|
2
|
+
const content: any;
|
|
3
|
+
export const ReactComponent: any;
|
|
4
|
+
export default content;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
declare module 'uuid';
|
|
@@ -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
|
+
}
|