create-nextblock 0.16.4 → 0.17.1
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/CLAUDE.md +22 -0
- 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/package.json +1 -1
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/CLAUDE.md +26 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- 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/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- 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 +272 -205
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +10473 -8467
- 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/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +442 -441
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -3
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
- 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/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- 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/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/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/pages/page.tsx +273 -273
- package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- 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/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/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
- 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/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/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/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- 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/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
- package/templates/nextblock-template/components/media/youtube-embed-replace.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 +471 -471
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- 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/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +1 -1
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +402 -282
- package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +15 -8
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +23 -4
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +4 -1
- package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +8 -0
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +2 -2
- package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +95 -104
- 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/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- 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-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/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
- package/templates/nextblock-template/lib/media/youtube.ts +99 -99
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- 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/seo/page-document.ts +4 -4
- package/templates/nextblock-template/lib/seo/redirect-store.ts +3 -2
- 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 +15 -175
- 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/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- 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/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +1 -0
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
- package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
- package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- 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/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
|
@@ -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,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.
|