create-nextblock 0.16.4 → 0.17.0
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 +270 -205
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +8096 -7198
- 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/04-DATABASE-AND-AUTH.md +11 -0
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -0
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- 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/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 +42 -12
- 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,222 +1,222 @@
|
|
|
1
|
-
# 10 Custom Blocks (Data-Driven CRUD)
|
|
2
|
-
|
|
3
|
-
NextBlock lets editors create their own block types at runtime, directly from
|
|
4
|
-
the CMS, with no code deploy. A custom block is defined as data — typed fields
|
|
5
|
-
plus a recursive layout schema — stored in Supabase and rendered on the public
|
|
6
|
-
site by a dynamic layout engine instead of a compiled React component.
|
|
7
|
-
|
|
8
|
-
This is a separate, complementary system to the code-defined built-in blocks in
|
|
9
|
-
`apps/nextblock/lib/blocks/blockRegistry.ts`. See
|
|
10
|
-
[03-CMS-AND-EDITOR.md](./03-CMS-AND-EDITOR.md) for the built-in block system and
|
|
11
|
-
[07-BLOCK-SDK-AND-EXTENSIBILITY.md](./07-BLOCK-SDK-AND-EXTENSIBILITY.md) for how
|
|
12
|
-
the three extensibility layers relate.
|
|
13
|
-
|
|
14
|
-
## The Core Idea
|
|
15
|
-
|
|
16
|
-
- A **custom block definition** is a row in `custom_block_definitions`.
|
|
17
|
-
- A **custom block instance** is just an ordinary `blocks` row whose
|
|
18
|
-
`block_type` equals a definition's `slug`.
|
|
19
|
-
|
|
20
|
-
Because an instance is a normal block, custom blocks drop into the page builder
|
|
21
|
-
exactly like built-ins: they can sit at the top level of a page/post or nest
|
|
22
|
-
inside `section` columns, and they participate in ordering, drag-and-drop, and
|
|
23
|
-
revisions without special-casing.
|
|
24
|
-
|
|
25
|
-
## Data Model
|
|
26
|
-
|
|
27
|
-
The table is created in
|
|
28
|
-
`libs/db/src/supabase/migrations/00000000000023_setup_custom_block_definitions.sql`.
|
|
29
|
-
|
|
30
|
-
`public.custom_block_definitions`:
|
|
31
|
-
|
|
32
|
-
| Column | Notes |
|
|
33
|
-
| :-- | :-- |
|
|
34
|
-
| `id` | `uuid` primary key |
|
|
35
|
-
| `slug` | unique, `^[a-z][a-z0-9-]*$`; this is the block instance's `block_type` |
|
|
36
|
-
| `name` | display name (non-empty) |
|
|
37
|
-
| `description` | optional, defaults to `''` |
|
|
38
|
-
| `fields` | `jsonb` field declarations; DB `CHECK` via `is_valid_custom_block_fields()` |
|
|
39
|
-
| `layout_schema` | `jsonb` layout tree; DB `CHECK` via `is_valid_custom_block_layout_schema()` |
|
|
40
|
-
| `is_original` | `false` when the row was produced by duplicating another definition |
|
|
41
|
-
|
|
42
|
-
The migration also defines:
|
|
43
|
-
|
|
44
|
-
- `is_valid_custom_block_fields(jsonb)` and
|
|
45
|
-
`is_valid_custom_block_layout_schema(jsonb)` — immutable validation functions
|
|
46
|
-
used as table `CHECK` constraints, so malformed definitions are rejected at
|
|
47
|
-
the database layer even if application validation is bypassed.
|
|
48
|
-
- `duplicate_block_definition(target_id uuid)` — `SECURITY DEFINER` RPC that
|
|
49
|
-
copies a definition, auto-suffixing the slug (`-copy`, `-copy-2`, …), naming
|
|
50
|
-
it `"<name> Copy"`, and setting `is_original = false`. Restricted to
|
|
51
|
-
`ADMIN`/`WRITER` (or `service_role`).
|
|
52
|
-
|
|
53
|
-
### Row Level Security
|
|
54
|
-
|
|
55
|
-
- Public `SELECT` (definitions must be readable to render on the public site).
|
|
56
|
-
- `INSERT` / `UPDATE` / `DELETE` for authenticated users whose role is `ADMIN`
|
|
57
|
-
or `WRITER`.
|
|
58
|
-
- Full access for `service_role`.
|
|
59
|
-
|
|
60
|
-
## Field Types
|
|
61
|
-
|
|
62
|
-
Application-side schemas live in `libs/utils/src/lib/custom-blocks.ts` and are
|
|
63
|
-
exported from `@nextblock-cms/utils`. Every field shares a base of `key`
|
|
64
|
-
(`^[a-z][a-z0-9_]*$`, unique within a block), `label`, optional `description`,
|
|
65
|
-
and `required`. The `type` discriminates four variants:
|
|
66
|
-
|
|
67
|
-
| Type | Purpose | Notable options |
|
|
68
|
-
| :-- | :-- | :-- |
|
|
69
|
-
| `text` | single-line / plain text | `default_value`, `placeholder`, `min_length`, `max_length` |
|
|
70
|
-
| `rich-text` | HTML rich text | `default_value`, `placeholder` |
|
|
71
|
-
| `image_r2` | image stored in R2 | `accept[]`, `max_bytes`, `default_value` = `{ object_key, url, alt, width, height, … }` |
|
|
72
|
-
| `db_relation` | reference rows in a table | `table`, `value_column` (default `id`), `display_column` (default `title`), `multiple`, `filters` |
|
|
73
|
-
|
|
74
|
-
The CMS authoring components map onto these types: `ImageR2Picker` for
|
|
75
|
-
`image_r2`, `DBRelationSelect` for `db_relation`, and the rich-text editor for
|
|
76
|
-
`rich-text`.
|
|
77
|
-
|
|
78
|
-
## Layout Schema
|
|
79
|
-
|
|
80
|
-
`layout_schema` is a recursive discriminated union (`customBlockLayoutNodeSchema`)
|
|
81
|
-
with two node types:
|
|
82
|
-
|
|
83
|
-
- **`container`** — `{ type: 'container', as?, className?, children: [] }`.
|
|
84
|
-
Groups other nodes.
|
|
85
|
-
- **`field_render`** — `{ type: 'field_render', field_key, as?, className?,
|
|
86
|
-
column?, emptyFallback? }`. Renders a single field's value.
|
|
87
|
-
|
|
88
|
-
Rules and helpers:
|
|
89
|
-
|
|
90
|
-
- `as` is restricted to a safe HTML element set (`article`, `aside`,
|
|
91
|
-
`blockquote`, `div`, `figure`, `figcaption`, `h2`, `h3`, `img`, `p`,
|
|
92
|
-
`section`, `span`).
|
|
93
|
-
- `className` accepts Tailwind utility classes.
|
|
94
|
-
- `column` lets a `field_render` bound to a `db_relation` field surface a
|
|
95
|
-
specific column of the resolved record, so one relation field can render
|
|
96
|
-
several columns (e.g. a product's title and price). `emptyFallback` renders
|
|
97
|
-
when the value is empty.
|
|
98
|
-
- Every `field_key` referenced in the layout must exist in `fields`
|
|
99
|
-
(`assertLayoutFieldKeysExist` enforces this in `customBlockDefinitionCreateSchema`).
|
|
100
|
-
- `orderCustomBlockFieldsByLayout()` orders fields to match the layout's
|
|
101
|
-
depth-first `field_render` order for the editor form; `buildCustomBlockCopySlug()`
|
|
102
|
-
mirrors the SQL duplicate-slug logic on the client.
|
|
103
|
-
|
|
104
|
-
Exported Zod surfaces: `customBlockDefinitionCreateSchema`,
|
|
105
|
-
`customBlockDefinitionUpdateSchema`, and `customBlockDefinitionRowSchema`, with
|
|
106
|
-
inferred types `CustomBlockDefinition`, `CustomBlockDefinitionCreateInput`, and
|
|
107
|
-
`CustomBlockDefinitionUpdateInput`.
|
|
108
|
-
|
|
109
|
-
## CMS CRUD Surface
|
|
110
|
-
|
|
111
|
-
Everything lives under `apps/nextblock/app/cms/custom-blocks`:
|
|
112
|
-
|
|
113
|
-
- `page.tsx` — searchable grid/list of definitions with duplicate and delete.
|
|
114
|
-
- `new/page.tsx` and `[id]/edit/page.tsx` — authoring screens.
|
|
115
|
-
- `components/BlockComposer.tsx` — the field + layout composer.
|
|
116
|
-
- `components/DBRelationSelect.tsx`, `components/ImageR2Picker.tsx` — field-type
|
|
117
|
-
editors.
|
|
118
|
-
- `components/BlocksLibraryTransferControls.tsx` — import/export UI.
|
|
119
|
-
|
|
120
|
-
Server actions in `app/cms/custom-blocks/actions.ts` all gate on
|
|
121
|
-
`requireCmsWriter` (`ADMIN`/`WRITER`) and revalidate the
|
|
122
|
-
`custom-block-definitions` cache tag plus `/cms/blocks` and `/cms/custom-blocks`:
|
|
123
|
-
|
|
124
|
-
- `listCustomBlockDefinitions`, `getCustomBlockDefinition`
|
|
125
|
-
- `createCustomBlockDefinition`, `updateCustomBlockDefinition`,
|
|
126
|
-
`deleteCustomBlockDefinition`
|
|
127
|
-
- `duplicateCustomBlockDefinition` (calls the `duplicate_block_definition` RPC)
|
|
128
|
-
- `exportBlocksLibraryAction`, `dryRunBlocksLibraryImportAction`,
|
|
129
|
-
`applyBlocksLibraryImportAction`
|
|
130
|
-
|
|
131
|
-
## Rendering
|
|
132
|
-
|
|
133
|
-
The loader is `apps/nextblock/lib/custom-block-definitions.ts`:
|
|
134
|
-
|
|
135
|
-
- `getCachedCustomBlockDefinitions()` and
|
|
136
|
-
`getCachedCustomBlockDefinitionBySlug()` read through `getSsgSupabaseClient`
|
|
137
|
-
and `unstable_cache` (tag `custom-block-definitions`, 60s revalidate).
|
|
138
|
-
- The by-slug path falls back to a live (uncached) read when the cache misses,
|
|
139
|
-
so a freshly saved block renders immediately instead of showing
|
|
140
|
-
"Unsupported block type" during the revalidation window.
|
|
141
|
-
|
|
142
|
-
`apps/nextblock/components/BlockRenderer.tsx` dispatches blocks: if a
|
|
143
|
-
`block_type` has no built-in renderer, it looks the slug up via
|
|
144
|
-
`getCachedCustomBlockDefinitionBySlug()` and renders through
|
|
145
|
-
`CachedDynamicLayoutEngine` (which wraps
|
|
146
|
-
`components/renderers/DynamicLayoutEngine.tsx`). The dynamic engine walks the
|
|
147
|
-
`layout_schema`, renders `container` nodes as their `as` element with the given
|
|
148
|
-
classes, and resolves each `field_render` against the instance content
|
|
149
|
-
(including `db_relation` lookups). If neither a built-in renderer nor a custom
|
|
150
|
-
definition matches, the renderer shows an "Unsupported block type" notice with
|
|
151
|
-
the offending slug.
|
|
152
|
-
|
|
153
|
-
CMS-side editing of an instance uses
|
|
154
|
-
`app/cms/blocks/editors/DynamicCustomBlockEditor.tsx` and
|
|
155
|
-
`app/cms/blocks/components/CustomBlockEditorPreview.tsx`.
|
|
156
|
-
|
|
157
|
-
## Import / Export / Backup / Restore
|
|
158
|
-
|
|
159
|
-
Custom blocks are portable as a JSON "Blocks Library" bundle. The backend is
|
|
160
|
-
`apps/nextblock/lib/cms-transfer/server.ts`:
|
|
161
|
-
|
|
162
|
-
- `exportBlocksLibraryBundle()` serializes all definitions.
|
|
163
|
-
- `dryRunBlocksLibraryImport()` previews an import without writing.
|
|
164
|
-
- `applyBlocksLibraryImport()` applies it.
|
|
165
|
-
|
|
166
|
-
A bundle entry (`BackupCustomBlockRecord` in
|
|
167
|
-
`apps/nextblock/lib/cms-transfer/types.ts`) carries `slug`, `name`,
|
|
168
|
-
`description`, `fields`, `layout_schema`, and `is_original`. Imports run under a
|
|
169
|
-
conflict mode of `create_new` or `overwrite_existing`, and return a summary of
|
|
170
|
-
`created` / `updated` / `skipped` rows plus warnings and errors. Exports are
|
|
171
|
-
named `nextblock-blocks-library-<YYYY-MM-DD>.json`.
|
|
172
|
-
|
|
173
|
-
Custom blocks also ride along inside the broader content backup bundle
|
|
174
|
-
(`CmsBackupBundleV1.custom_blocks`, optional for backward compatibility),
|
|
175
|
-
surfaced at `/cms/import-export` via `ContentTransferControls.tsx`.
|
|
176
|
-
|
|
177
|
-
## Cortex AI "Build Widget"
|
|
178
|
-
|
|
179
|
-
Cortex AI can generate a custom block definition from a prompt:
|
|
180
|
-
|
|
181
|
-
- Route: `apps/nextblock/app/api/ai/cortex/build-widget/route.ts`.
|
|
182
|
-
- Helpers: `libs/cortex/src/lib/cortex-widget-registry.ts` and the custom-block
|
|
183
|
-
agent tools in `libs/cortex/src/lib/ai-global-agent-custom-block-tools.ts`.
|
|
184
|
-
- After a Cortex-driven change, the front end dispatches a
|
|
185
|
-
`nextblock:cortex-data-changed` event; the custom-blocks list listens for it
|
|
186
|
-
(and for window focus) and refetches so the library stays in sync without a
|
|
187
|
-
reload.
|
|
188
|
-
|
|
189
|
-
See [08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md](./08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md)
|
|
190
|
-
for the surrounding AI architecture and credential model.
|
|
191
|
-
|
|
192
|
-
## Verification
|
|
193
|
-
|
|
194
|
-
```bash
|
|
195
|
-
# Validate the custom block definition schemas / fixtures
|
|
196
|
-
npx tsx apps/nextblock/scripts/verify-custom-block-definitions.ts
|
|
197
|
-
|
|
198
|
-
# Exercise the dynamic layout engine
|
|
199
|
-
npx tsx apps/nextblock/scripts/verify-dynamic-layout-engine.tsx
|
|
200
|
-
|
|
201
|
-
# Cortex AI widget builder (needs OPENROUTER_API_KEY or stored BYOK)
|
|
202
|
-
npm run verify:cortex-ai-build-widget
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Relevant Vitest files:
|
|
206
|
-
|
|
207
|
-
- `apps/nextblock/components/renderers/DynamicLayoutEngine.test.tsx`
|
|
208
|
-
- `libs/cortex/src/lib/cortex-widget-registry.test.ts`
|
|
209
|
-
- `libs/cortex/src/lib/cortex-widget-schema.test.tsx`
|
|
210
|
-
|
|
211
|
-
## Notes for Contributors
|
|
212
|
-
|
|
213
|
-
- `custom_block_definitions` was added after the squashed migration baseline
|
|
214
|
-
(migration `00000000000023`). Per the production rule in
|
|
215
|
-
[05-DEVELOPER-GUIDE.md](./05-DEVELOPER-GUIDE.md) and the root `AGENTS.md`,
|
|
216
|
-
schema changes here must be new, forward-only migrations.
|
|
217
|
-
- After changing the table or seed data, regenerate the sandbox reset payload
|
|
218
|
-
with `npm run generate:sandbox` (the generated SQL already includes the
|
|
219
|
-
custom blocks migration).
|
|
220
|
-
- The slug is the public contract: renaming a definition's slug orphans every
|
|
221
|
-
existing instance that references the old slug (they fall back to
|
|
222
|
-
"Unsupported block type" until re-pointed).
|
|
1
|
+
# 10 Custom Blocks (Data-Driven CRUD)
|
|
2
|
+
|
|
3
|
+
NextBlock lets editors create their own block types at runtime, directly from
|
|
4
|
+
the CMS, with no code deploy. A custom block is defined as data — typed fields
|
|
5
|
+
plus a recursive layout schema — stored in Supabase and rendered on the public
|
|
6
|
+
site by a dynamic layout engine instead of a compiled React component.
|
|
7
|
+
|
|
8
|
+
This is a separate, complementary system to the code-defined built-in blocks in
|
|
9
|
+
`apps/nextblock/lib/blocks/blockRegistry.ts`. See
|
|
10
|
+
[03-CMS-AND-EDITOR.md](./03-CMS-AND-EDITOR.md) for the built-in block system and
|
|
11
|
+
[07-BLOCK-SDK-AND-EXTENSIBILITY.md](./07-BLOCK-SDK-AND-EXTENSIBILITY.md) for how
|
|
12
|
+
the three extensibility layers relate.
|
|
13
|
+
|
|
14
|
+
## The Core Idea
|
|
15
|
+
|
|
16
|
+
- A **custom block definition** is a row in `custom_block_definitions`.
|
|
17
|
+
- A **custom block instance** is just an ordinary `blocks` row whose
|
|
18
|
+
`block_type` equals a definition's `slug`.
|
|
19
|
+
|
|
20
|
+
Because an instance is a normal block, custom blocks drop into the page builder
|
|
21
|
+
exactly like built-ins: they can sit at the top level of a page/post or nest
|
|
22
|
+
inside `section` columns, and they participate in ordering, drag-and-drop, and
|
|
23
|
+
revisions without special-casing.
|
|
24
|
+
|
|
25
|
+
## Data Model
|
|
26
|
+
|
|
27
|
+
The table is created in
|
|
28
|
+
`libs/db/src/supabase/migrations/00000000000023_setup_custom_block_definitions.sql`.
|
|
29
|
+
|
|
30
|
+
`public.custom_block_definitions`:
|
|
31
|
+
|
|
32
|
+
| Column | Notes |
|
|
33
|
+
| :-- | :-- |
|
|
34
|
+
| `id` | `uuid` primary key |
|
|
35
|
+
| `slug` | unique, `^[a-z][a-z0-9-]*$`; this is the block instance's `block_type` |
|
|
36
|
+
| `name` | display name (non-empty) |
|
|
37
|
+
| `description` | optional, defaults to `''` |
|
|
38
|
+
| `fields` | `jsonb` field declarations; DB `CHECK` via `is_valid_custom_block_fields()` |
|
|
39
|
+
| `layout_schema` | `jsonb` layout tree; DB `CHECK` via `is_valid_custom_block_layout_schema()` |
|
|
40
|
+
| `is_original` | `false` when the row was produced by duplicating another definition |
|
|
41
|
+
|
|
42
|
+
The migration also defines:
|
|
43
|
+
|
|
44
|
+
- `is_valid_custom_block_fields(jsonb)` and
|
|
45
|
+
`is_valid_custom_block_layout_schema(jsonb)` — immutable validation functions
|
|
46
|
+
used as table `CHECK` constraints, so malformed definitions are rejected at
|
|
47
|
+
the database layer even if application validation is bypassed.
|
|
48
|
+
- `duplicate_block_definition(target_id uuid)` — `SECURITY DEFINER` RPC that
|
|
49
|
+
copies a definition, auto-suffixing the slug (`-copy`, `-copy-2`, …), naming
|
|
50
|
+
it `"<name> Copy"`, and setting `is_original = false`. Restricted to
|
|
51
|
+
`ADMIN`/`WRITER` (or `service_role`).
|
|
52
|
+
|
|
53
|
+
### Row Level Security
|
|
54
|
+
|
|
55
|
+
- Public `SELECT` (definitions must be readable to render on the public site).
|
|
56
|
+
- `INSERT` / `UPDATE` / `DELETE` for authenticated users whose role is `ADMIN`
|
|
57
|
+
or `WRITER`.
|
|
58
|
+
- Full access for `service_role`.
|
|
59
|
+
|
|
60
|
+
## Field Types
|
|
61
|
+
|
|
62
|
+
Application-side schemas live in `libs/utils/src/lib/custom-blocks.ts` and are
|
|
63
|
+
exported from `@nextblock-cms/utils`. Every field shares a base of `key`
|
|
64
|
+
(`^[a-z][a-z0-9_]*$`, unique within a block), `label`, optional `description`,
|
|
65
|
+
and `required`. The `type` discriminates four variants:
|
|
66
|
+
|
|
67
|
+
| Type | Purpose | Notable options |
|
|
68
|
+
| :-- | :-- | :-- |
|
|
69
|
+
| `text` | single-line / plain text | `default_value`, `placeholder`, `min_length`, `max_length` |
|
|
70
|
+
| `rich-text` | HTML rich text | `default_value`, `placeholder` |
|
|
71
|
+
| `image_r2` | image stored in R2 | `accept[]`, `max_bytes`, `default_value` = `{ object_key, url, alt, width, height, … }` |
|
|
72
|
+
| `db_relation` | reference rows in a table | `table`, `value_column` (default `id`), `display_column` (default `title`), `multiple`, `filters` |
|
|
73
|
+
|
|
74
|
+
The CMS authoring components map onto these types: `ImageR2Picker` for
|
|
75
|
+
`image_r2`, `DBRelationSelect` for `db_relation`, and the rich-text editor for
|
|
76
|
+
`rich-text`.
|
|
77
|
+
|
|
78
|
+
## Layout Schema
|
|
79
|
+
|
|
80
|
+
`layout_schema` is a recursive discriminated union (`customBlockLayoutNodeSchema`)
|
|
81
|
+
with two node types:
|
|
82
|
+
|
|
83
|
+
- **`container`** — `{ type: 'container', as?, className?, children: [] }`.
|
|
84
|
+
Groups other nodes.
|
|
85
|
+
- **`field_render`** — `{ type: 'field_render', field_key, as?, className?,
|
|
86
|
+
column?, emptyFallback? }`. Renders a single field's value.
|
|
87
|
+
|
|
88
|
+
Rules and helpers:
|
|
89
|
+
|
|
90
|
+
- `as` is restricted to a safe HTML element set (`article`, `aside`,
|
|
91
|
+
`blockquote`, `div`, `figure`, `figcaption`, `h2`, `h3`, `img`, `p`,
|
|
92
|
+
`section`, `span`).
|
|
93
|
+
- `className` accepts Tailwind utility classes.
|
|
94
|
+
- `column` lets a `field_render` bound to a `db_relation` field surface a
|
|
95
|
+
specific column of the resolved record, so one relation field can render
|
|
96
|
+
several columns (e.g. a product's title and price). `emptyFallback` renders
|
|
97
|
+
when the value is empty.
|
|
98
|
+
- Every `field_key` referenced in the layout must exist in `fields`
|
|
99
|
+
(`assertLayoutFieldKeysExist` enforces this in `customBlockDefinitionCreateSchema`).
|
|
100
|
+
- `orderCustomBlockFieldsByLayout()` orders fields to match the layout's
|
|
101
|
+
depth-first `field_render` order for the editor form; `buildCustomBlockCopySlug()`
|
|
102
|
+
mirrors the SQL duplicate-slug logic on the client.
|
|
103
|
+
|
|
104
|
+
Exported Zod surfaces: `customBlockDefinitionCreateSchema`,
|
|
105
|
+
`customBlockDefinitionUpdateSchema`, and `customBlockDefinitionRowSchema`, with
|
|
106
|
+
inferred types `CustomBlockDefinition`, `CustomBlockDefinitionCreateInput`, and
|
|
107
|
+
`CustomBlockDefinitionUpdateInput`.
|
|
108
|
+
|
|
109
|
+
## CMS CRUD Surface
|
|
110
|
+
|
|
111
|
+
Everything lives under `apps/nextblock/app/cms/custom-blocks`:
|
|
112
|
+
|
|
113
|
+
- `page.tsx` — searchable grid/list of definitions with duplicate and delete.
|
|
114
|
+
- `new/page.tsx` and `[id]/edit/page.tsx` — authoring screens.
|
|
115
|
+
- `components/BlockComposer.tsx` — the field + layout composer.
|
|
116
|
+
- `components/DBRelationSelect.tsx`, `components/ImageR2Picker.tsx` — field-type
|
|
117
|
+
editors.
|
|
118
|
+
- `components/BlocksLibraryTransferControls.tsx` — import/export UI.
|
|
119
|
+
|
|
120
|
+
Server actions in `app/cms/custom-blocks/actions.ts` all gate on
|
|
121
|
+
`requireCmsWriter` (`ADMIN`/`WRITER`) and revalidate the
|
|
122
|
+
`custom-block-definitions` cache tag plus `/cms/blocks` and `/cms/custom-blocks`:
|
|
123
|
+
|
|
124
|
+
- `listCustomBlockDefinitions`, `getCustomBlockDefinition`
|
|
125
|
+
- `createCustomBlockDefinition`, `updateCustomBlockDefinition`,
|
|
126
|
+
`deleteCustomBlockDefinition`
|
|
127
|
+
- `duplicateCustomBlockDefinition` (calls the `duplicate_block_definition` RPC)
|
|
128
|
+
- `exportBlocksLibraryAction`, `dryRunBlocksLibraryImportAction`,
|
|
129
|
+
`applyBlocksLibraryImportAction`
|
|
130
|
+
|
|
131
|
+
## Rendering
|
|
132
|
+
|
|
133
|
+
The loader is `apps/nextblock/lib/custom-block-definitions.ts`:
|
|
134
|
+
|
|
135
|
+
- `getCachedCustomBlockDefinitions()` and
|
|
136
|
+
`getCachedCustomBlockDefinitionBySlug()` read through `getSsgSupabaseClient`
|
|
137
|
+
and `unstable_cache` (tag `custom-block-definitions`, 60s revalidate).
|
|
138
|
+
- The by-slug path falls back to a live (uncached) read when the cache misses,
|
|
139
|
+
so a freshly saved block renders immediately instead of showing
|
|
140
|
+
"Unsupported block type" during the revalidation window.
|
|
141
|
+
|
|
142
|
+
`apps/nextblock/components/BlockRenderer.tsx` dispatches blocks: if a
|
|
143
|
+
`block_type` has no built-in renderer, it looks the slug up via
|
|
144
|
+
`getCachedCustomBlockDefinitionBySlug()` and renders through
|
|
145
|
+
`CachedDynamicLayoutEngine` (which wraps
|
|
146
|
+
`components/renderers/DynamicLayoutEngine.tsx`). The dynamic engine walks the
|
|
147
|
+
`layout_schema`, renders `container` nodes as their `as` element with the given
|
|
148
|
+
classes, and resolves each `field_render` against the instance content
|
|
149
|
+
(including `db_relation` lookups). If neither a built-in renderer nor a custom
|
|
150
|
+
definition matches, the renderer shows an "Unsupported block type" notice with
|
|
151
|
+
the offending slug.
|
|
152
|
+
|
|
153
|
+
CMS-side editing of an instance uses
|
|
154
|
+
`app/cms/blocks/editors/DynamicCustomBlockEditor.tsx` and
|
|
155
|
+
`app/cms/blocks/components/CustomBlockEditorPreview.tsx`.
|
|
156
|
+
|
|
157
|
+
## Import / Export / Backup / Restore
|
|
158
|
+
|
|
159
|
+
Custom blocks are portable as a JSON "Blocks Library" bundle. The backend is
|
|
160
|
+
`apps/nextblock/lib/cms-transfer/server.ts`:
|
|
161
|
+
|
|
162
|
+
- `exportBlocksLibraryBundle()` serializes all definitions.
|
|
163
|
+
- `dryRunBlocksLibraryImport()` previews an import without writing.
|
|
164
|
+
- `applyBlocksLibraryImport()` applies it.
|
|
165
|
+
|
|
166
|
+
A bundle entry (`BackupCustomBlockRecord` in
|
|
167
|
+
`apps/nextblock/lib/cms-transfer/types.ts`) carries `slug`, `name`,
|
|
168
|
+
`description`, `fields`, `layout_schema`, and `is_original`. Imports run under a
|
|
169
|
+
conflict mode of `create_new` or `overwrite_existing`, and return a summary of
|
|
170
|
+
`created` / `updated` / `skipped` rows plus warnings and errors. Exports are
|
|
171
|
+
named `nextblock-blocks-library-<YYYY-MM-DD>.json`.
|
|
172
|
+
|
|
173
|
+
Custom blocks also ride along inside the broader content backup bundle
|
|
174
|
+
(`CmsBackupBundleV1.custom_blocks`, optional for backward compatibility),
|
|
175
|
+
surfaced at `/cms/import-export` via `ContentTransferControls.tsx`.
|
|
176
|
+
|
|
177
|
+
## Cortex AI "Build Widget"
|
|
178
|
+
|
|
179
|
+
Cortex AI can generate a custom block definition from a prompt:
|
|
180
|
+
|
|
181
|
+
- Route: `apps/nextblock/app/api/ai/cortex/build-widget/route.ts`.
|
|
182
|
+
- Helpers: `libs/cortex/src/lib/cortex-widget-registry.ts` and the custom-block
|
|
183
|
+
agent tools in `libs/cortex/src/lib/ai-global-agent-custom-block-tools.ts`.
|
|
184
|
+
- After a Cortex-driven change, the front end dispatches a
|
|
185
|
+
`nextblock:cortex-data-changed` event; the custom-blocks list listens for it
|
|
186
|
+
(and for window focus) and refetches so the library stays in sync without a
|
|
187
|
+
reload.
|
|
188
|
+
|
|
189
|
+
See [08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md](./08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md)
|
|
190
|
+
for the surrounding AI architecture and credential model.
|
|
191
|
+
|
|
192
|
+
## Verification
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Validate the custom block definition schemas / fixtures
|
|
196
|
+
npx tsx apps/nextblock/scripts/verify-custom-block-definitions.ts
|
|
197
|
+
|
|
198
|
+
# Exercise the dynamic layout engine
|
|
199
|
+
npx tsx apps/nextblock/scripts/verify-dynamic-layout-engine.tsx
|
|
200
|
+
|
|
201
|
+
# Cortex AI widget builder (needs OPENROUTER_API_KEY or stored BYOK)
|
|
202
|
+
npm run verify:cortex-ai-build-widget
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Relevant Vitest files:
|
|
206
|
+
|
|
207
|
+
- `apps/nextblock/components/renderers/DynamicLayoutEngine.test.tsx`
|
|
208
|
+
- `libs/cortex/src/lib/cortex-widget-registry.test.ts`
|
|
209
|
+
- `libs/cortex/src/lib/cortex-widget-schema.test.tsx`
|
|
210
|
+
|
|
211
|
+
## Notes for Contributors
|
|
212
|
+
|
|
213
|
+
- `custom_block_definitions` was added after the squashed migration baseline
|
|
214
|
+
(migration `00000000000023`). Per the production rule in
|
|
215
|
+
[05-DEVELOPER-GUIDE.md](./05-DEVELOPER-GUIDE.md) and the root `AGENTS.md`,
|
|
216
|
+
schema changes here must be new, forward-only migrations.
|
|
217
|
+
- After changing the table or seed data, regenerate the sandbox reset payload
|
|
218
|
+
with `npm run generate:sandbox` (the generated SQL already includes the
|
|
219
|
+
custom blocks migration).
|
|
220
|
+
- The slug is the public contract: renaming a definition's slug orphans every
|
|
221
|
+
existing instance that references the old slug (they fall back to
|
|
222
|
+
"Unsupported block type" until re-pointed).
|
|
@@ -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
|
+
}
|