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.
Files changed (196) hide show
  1. package/CLAUDE.md +22 -0
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/package.json +1 -1
  8. package/templates/nextblock-template/.browserslistrc +11 -11
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/CLAUDE.md +26 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  14. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  15. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  16. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  17. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  18. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  19. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  20. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  21. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  22. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  23. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +270 -205
  24. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +8096 -7198
  25. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  26. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  27. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  28. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  29. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  30. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  31. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  32. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +442 -441
  33. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
  34. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  36. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  37. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  38. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  39. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  40. package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -3
  41. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  42. package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
  43. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  44. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  45. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  46. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  47. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  48. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  49. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  50. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  51. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  52. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  53. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  54. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  55. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  56. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  57. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  58. package/templates/nextblock-template/app/cms/pages/page.tsx +273 -273
  59. package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
  60. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  62. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  63. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  64. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  65. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  66. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  67. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  68. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  69. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  70. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  71. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  72. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  73. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  74. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  75. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  76. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  77. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  78. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  79. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  80. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  81. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  82. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  83. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  84. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  85. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  86. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  87. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  88. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  89. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  90. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  91. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  92. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  93. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  94. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  95. package/templates/nextblock-template/components/Header.tsx +38 -38
  96. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  97. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  98. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  99. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  100. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  101. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  102. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  103. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
  104. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  105. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  106. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  107. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  108. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
  109. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
  110. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  111. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  112. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  113. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  114. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
  115. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  116. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  117. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  118. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  119. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  120. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  121. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +11 -0
  122. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  123. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -0
  124. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  125. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  126. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  127. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  128. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  129. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  130. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  131. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
  132. package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
  133. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  134. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  135. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  136. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  137. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  138. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  139. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  140. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  141. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  142. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  143. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  144. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  145. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  146. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  147. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  148. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  149. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  150. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  151. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
  152. package/templates/nextblock-template/lib/media/youtube.ts +99 -99
  153. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  154. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  155. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  156. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  157. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  158. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  159. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  160. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  161. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  162. package/templates/nextblock-template/lib/seo/page-document.ts +4 -4
  163. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  164. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  165. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  166. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +42 -12
  167. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  168. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  169. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  170. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  171. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  172. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  173. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  174. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  175. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  176. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  177. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  178. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  179. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  180. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  181. package/templates/nextblock-template/next-env.d.ts +1 -0
  182. package/templates/nextblock-template/package.json +1 -1
  183. package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
  184. package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
  185. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  186. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  187. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  188. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  189. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  190. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  191. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  192. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  193. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  194. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  195. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  196. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
@@ -1,105 +1,105 @@
1
- import 'server-only';
2
- // Server-only read/write for the `system_configuration` singleton table.
3
- // Mirrors lib/privacy/settings.ts, but the data lives in a dedicated ADMIN-only
4
- // table (migration 00000000000030) rather than the public site_settings store.
5
- import { createClient, getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
6
- import type { Json } from '@nextblock-cms/db';
7
- import { DEFAULT_SYSTEM_CONFIGURATION, type SystemConfiguration } from './types';
8
-
9
- function asBool(value: unknown, fallback: boolean): boolean {
10
- if (typeof value === 'boolean') return value;
11
- if (typeof value === 'string') return value === 'true' || value === 'on';
12
- return fallback;
13
- }
14
-
15
- function asSettings(value: unknown): Record<string, unknown> {
16
- return value && typeof value === 'object' && !Array.isArray(value)
17
- ? (value as Record<string, unknown>)
18
- : {};
19
- }
20
-
21
- type ConfigPatch = Partial<Pick<SystemConfiguration, 'auto_accept_signups'>> & {
22
- settings?: Record<string, unknown>;
23
- };
24
-
25
- /** Build a row payload, casting the loosely-typed settings object to the DB Json type. */
26
- function toRow(patch: ConfigPatch): {
27
- auto_accept_signups?: boolean;
28
- settings?: Json;
29
- updated_at: string;
30
- } {
31
- const row: { auto_accept_signups?: boolean; settings?: Json; updated_at: string } = {
32
- updated_at: new Date().toISOString(),
33
- };
34
- if (patch.auto_accept_signups !== undefined) {
35
- row.auto_accept_signups = patch.auto_accept_signups;
36
- }
37
- if (patch.settings !== undefined) {
38
- row.settings = patch.settings as unknown as Json;
39
- }
40
- return row;
41
- }
42
-
43
- /**
44
- * Read the singleton config. Uses the service-role client so it works from any
45
- * context — including the anonymous public sign-up path, which must read
46
- * `auto_accept_signups` even though the table is otherwise ADMIN-only. Falls back to
47
- * safe defaults when the service-role key is absent (unconfigured instance).
48
- */
49
- export async function getSystemConfiguration(): Promise<SystemConfiguration> {
50
- let supabase: ReturnType<typeof getServiceRoleSupabaseClient>;
51
- try {
52
- supabase = getServiceRoleSupabaseClient();
53
- } catch {
54
- return DEFAULT_SYSTEM_CONFIGURATION;
55
- }
56
-
57
- const { data, error } = await supabase
58
- .from('system_configuration')
59
- .select('auto_accept_signups, settings')
60
- .eq('id', 1)
61
- .maybeSingle();
62
-
63
- if (error || !data) {
64
- return DEFAULT_SYSTEM_CONFIGURATION;
65
- }
66
-
67
- return {
68
- auto_accept_signups: asBool(data.auto_accept_signups, false),
69
- settings: asSettings(data.settings),
70
- };
71
- }
72
-
73
- /**
74
- * Persist a partial update via the request-scoped client. RLS enforces ADMIN — used
75
- * by the CMS security settings page. The wizard (which runs before any admin exists)
76
- * uses setSystemConfigurationServiceRole instead.
77
- */
78
- export async function updateSystemConfiguration(patch: ConfigPatch): Promise<void> {
79
- const supabase = createClient();
80
- const { error } = await supabase
81
- .from('system_configuration')
82
- .update(toRow(patch))
83
- .eq('id', 1);
84
-
85
- if (error) {
86
- console.error('Error saving system configuration:', error.message);
87
- throw new Error('Failed to save system configuration.');
88
- }
89
- }
90
-
91
- /**
92
- * Seed/update the singleton with the service-role client (bypasses RLS). Used by the
93
- * /setup wizard before the first admin exists.
94
- */
95
- export async function setSystemConfigurationServiceRole(patch: ConfigPatch): Promise<void> {
96
- const supabase = getServiceRoleSupabaseClient();
97
- const { error } = await supabase
98
- .from('system_configuration')
99
- .upsert({ id: 1, ...toRow(patch) });
100
-
101
- if (error) {
102
- console.error('Error seeding system configuration:', error.message);
103
- throw new Error('Failed to seed system configuration.');
104
- }
105
- }
1
+ import 'server-only';
2
+ // Server-only read/write for the `system_configuration` singleton table.
3
+ // Mirrors lib/privacy/settings.ts, but the data lives in a dedicated ADMIN-only
4
+ // table (migration 00000000000030) rather than the public site_settings store.
5
+ import { createClient, getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
6
+ import type { Json } from '@nextblock-cms/db';
7
+ import { DEFAULT_SYSTEM_CONFIGURATION, type SystemConfiguration } from './types';
8
+
9
+ function asBool(value: unknown, fallback: boolean): boolean {
10
+ if (typeof value === 'boolean') return value;
11
+ if (typeof value === 'string') return value === 'true' || value === 'on';
12
+ return fallback;
13
+ }
14
+
15
+ function asSettings(value: unknown): Record<string, unknown> {
16
+ return value && typeof value === 'object' && !Array.isArray(value)
17
+ ? (value as Record<string, unknown>)
18
+ : {};
19
+ }
20
+
21
+ type ConfigPatch = Partial<Pick<SystemConfiguration, 'auto_accept_signups'>> & {
22
+ settings?: Record<string, unknown>;
23
+ };
24
+
25
+ /** Build a row payload, casting the loosely-typed settings object to the DB Json type. */
26
+ function toRow(patch: ConfigPatch): {
27
+ auto_accept_signups?: boolean;
28
+ settings?: Json;
29
+ updated_at: string;
30
+ } {
31
+ const row: { auto_accept_signups?: boolean; settings?: Json; updated_at: string } = {
32
+ updated_at: new Date().toISOString(),
33
+ };
34
+ if (patch.auto_accept_signups !== undefined) {
35
+ row.auto_accept_signups = patch.auto_accept_signups;
36
+ }
37
+ if (patch.settings !== undefined) {
38
+ row.settings = patch.settings as unknown as Json;
39
+ }
40
+ return row;
41
+ }
42
+
43
+ /**
44
+ * Read the singleton config. Uses the service-role client so it works from any
45
+ * context — including the anonymous public sign-up path, which must read
46
+ * `auto_accept_signups` even though the table is otherwise ADMIN-only. Falls back to
47
+ * safe defaults when the service-role key is absent (unconfigured instance).
48
+ */
49
+ export async function getSystemConfiguration(): Promise<SystemConfiguration> {
50
+ let supabase: ReturnType<typeof getServiceRoleSupabaseClient>;
51
+ try {
52
+ supabase = getServiceRoleSupabaseClient();
53
+ } catch {
54
+ return DEFAULT_SYSTEM_CONFIGURATION;
55
+ }
56
+
57
+ const { data, error } = await supabase
58
+ .from('system_configuration')
59
+ .select('auto_accept_signups, settings')
60
+ .eq('id', 1)
61
+ .maybeSingle();
62
+
63
+ if (error || !data) {
64
+ return DEFAULT_SYSTEM_CONFIGURATION;
65
+ }
66
+
67
+ return {
68
+ auto_accept_signups: asBool(data.auto_accept_signups, false),
69
+ settings: asSettings(data.settings),
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Persist a partial update via the request-scoped client. RLS enforces ADMIN — used
75
+ * by the CMS security settings page. The wizard (which runs before any admin exists)
76
+ * uses setSystemConfigurationServiceRole instead.
77
+ */
78
+ export async function updateSystemConfiguration(patch: ConfigPatch): Promise<void> {
79
+ const supabase = createClient();
80
+ const { error } = await supabase
81
+ .from('system_configuration')
82
+ .update(toRow(patch))
83
+ .eq('id', 1);
84
+
85
+ if (error) {
86
+ console.error('Error saving system configuration:', error.message);
87
+ throw new Error('Failed to save system configuration.');
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Seed/update the singleton with the service-role client (bypasses RLS). Used by the
93
+ * /setup wizard before the first admin exists.
94
+ */
95
+ export async function setSystemConfigurationServiceRole(patch: ConfigPatch): Promise<void> {
96
+ const supabase = getServiceRoleSupabaseClient();
97
+ const { error } = await supabase
98
+ .from('system_configuration')
99
+ .upsert({ id: 1, ...toRow(patch) });
100
+
101
+ if (error) {
102
+ console.error('Error seeding system configuration:', error.message);
103
+ throw new Error('Failed to seed system configuration.');
104
+ }
105
+ }
@@ -1,18 +1,18 @@
1
- // Pure, client-safe types for the system_configuration singleton.
2
- // No server-only dependencies — importable from client and server modules.
3
-
4
- export interface SystemConfiguration {
5
- /**
6
- * When true, new public sign-ups skip outbound email verification: the signup
7
- * route creates an already-confirmed account via the service role instead of
8
- * relying on an SMTP confirmation link. Default false (defense-in-depth).
9
- */
10
- auto_accept_signups: boolean;
11
- /** Forward-compatible feature toggles. Never holds secrets. */
12
- settings: Record<string, unknown>;
13
- }
14
-
15
- export const DEFAULT_SYSTEM_CONFIGURATION: SystemConfiguration = {
16
- auto_accept_signups: false,
17
- settings: {},
18
- };
1
+ // Pure, client-safe types for the system_configuration singleton.
2
+ // No server-only dependencies — importable from client and server modules.
3
+
4
+ export interface SystemConfiguration {
5
+ /**
6
+ * When true, new public sign-ups skip outbound email verification: the signup
7
+ * route creates an already-confirmed account via the service role instead of
8
+ * relying on an SMTP confirmation link. Default false (defense-in-depth).
9
+ */
10
+ auto_accept_signups: boolean;
11
+ /** Forward-compatible feature toggles. Never holds secrets. */
12
+ settings: Record<string, unknown>;
13
+ }
14
+
15
+ export const DEFAULT_SYSTEM_CONFIGURATION: SystemConfiguration = {
16
+ auto_accept_signups: false,
17
+ settings: {},
18
+ };
@@ -1,66 +1,66 @@
1
- import 'server-only';
2
-
3
- import { resolveSupabaseServiceKey, resolveSupabaseUrl } from '../setup/env-status';
4
-
5
- // NextBlock has two storage backends:
6
- //
7
- // 's3' — the existing S3-compatible path (Cloudflare R2, self-hosted MinIO, or
8
- // even Supabase Storage's S3 endpoint when you create S3 access keys).
9
- // Driven by the R2_* env vars + @aws-sdk/client-s3. Unchanged.
10
- // 'supabase' — native Supabase Storage via the supabase-js client and the injected
11
- // service-role/secret key. NO S3 access keys required. This is what makes
12
- // the one-click Vercel deploy work with zero storage configuration: the
13
- // Marketplace integration injects the Supabase URL + secret key but NOT S3
14
- // keys, so the S3 path can't authenticate — the native API can.
15
- //
16
- // The S3 path always wins when its keys are present, so local dev, Docker/MinIO, and BYO
17
- // Cloudflare R2 are completely unaffected. The native path is only chosen as the zero-key
18
- // fallback for a connected Supabase project.
19
-
20
- export type StorageBackend = 's3' | 'supabase';
21
-
22
- /** Which storage backend to use. Explicit STORAGE_PROVIDER wins; otherwise inferred. */
23
- export function getStorageBackend(): StorageBackend {
24
- const explicit = process.env.STORAGE_PROVIDER?.trim().toLowerCase();
25
- if (explicit === 'supabase') return 'supabase';
26
- if (explicit === 's3' || explicit === 'r2' || explicit === 'minio') return 's3';
27
-
28
- // S3/R2/MinIO wins whenever its credentials exist (local, Docker, BYO R2, or
29
- // Supabase-S3-with-keys). Only fall back to native Supabase Storage when there are no
30
- // S3 keys but a Supabase project IS connected (service key + URL) — the Vercel case.
31
- const hasS3Keys = Boolean(
32
- process.env.R2_ACCESS_KEY_ID && process.env.R2_SECRET_ACCESS_KEY,
33
- );
34
- if (hasS3Keys) return 's3';
35
- if (resolveSupabaseServiceKey() && resolveSupabaseUrl()) return 'supabase';
36
- return 's3';
37
- }
38
-
39
- /** The bucket name to read/write. Defaults to "media" on the native Supabase backend. */
40
- export function getStorageBucket(): string {
41
- const explicit = process.env.STORAGE_BUCKET || process.env.R2_BUCKET_NAME;
42
- if (explicit) return explicit;
43
- return getStorageBackend() === 'supabase' ? 'media' : '';
44
- }
45
-
46
- /**
47
- * The public base URL objects are served from — what resolveMediaUrl() joins object keys
48
- * onto. An explicit R2 public/base URL always wins; otherwise, on the native Supabase
49
- * backend, derive Supabase Storage's public object endpoint
50
- * (`<project>/storage/v1/object/public/<bucket>`) so every display call site works with
51
- * zero extra config. Kept in sync with the CJS computation in next.config.js.
52
- */
53
- export function resolveMediaBaseUrl(): string {
54
- const explicit =
55
- process.env.NEXT_PUBLIC_R2_BASE_URL || process.env.NEXT_PUBLIC_R2_PUBLIC_URL;
56
- if (explicit) return explicit.replace(/\/+$/, '');
57
-
58
- if (getStorageBackend() === 'supabase') {
59
- const url = resolveSupabaseUrl();
60
- const bucket = getStorageBucket();
61
- if (url && bucket) {
62
- return `${url.replace(/\/+$/, '')}/storage/v1/object/public/${bucket}`;
63
- }
64
- }
65
- return '';
66
- }
1
+ import 'server-only';
2
+
3
+ import { resolveSupabaseServiceKey, resolveSupabaseUrl } from '../setup/env-status';
4
+
5
+ // NextBlock has two storage backends:
6
+ //
7
+ // 's3' — the existing S3-compatible path (Cloudflare R2, self-hosted MinIO, or
8
+ // even Supabase Storage's S3 endpoint when you create S3 access keys).
9
+ // Driven by the R2_* env vars + @aws-sdk/client-s3. Unchanged.
10
+ // 'supabase' — native Supabase Storage via the supabase-js client and the injected
11
+ // service-role/secret key. NO S3 access keys required. This is what makes
12
+ // the one-click Vercel deploy work with zero storage configuration: the
13
+ // Marketplace integration injects the Supabase URL + secret key but NOT S3
14
+ // keys, so the S3 path can't authenticate — the native API can.
15
+ //
16
+ // The S3 path always wins when its keys are present, so local dev, Docker/MinIO, and BYO
17
+ // Cloudflare R2 are completely unaffected. The native path is only chosen as the zero-key
18
+ // fallback for a connected Supabase project.
19
+
20
+ export type StorageBackend = 's3' | 'supabase';
21
+
22
+ /** Which storage backend to use. Explicit STORAGE_PROVIDER wins; otherwise inferred. */
23
+ export function getStorageBackend(): StorageBackend {
24
+ const explicit = process.env.STORAGE_PROVIDER?.trim().toLowerCase();
25
+ if (explicit === 'supabase') return 'supabase';
26
+ if (explicit === 's3' || explicit === 'r2' || explicit === 'minio') return 's3';
27
+
28
+ // S3/R2/MinIO wins whenever its credentials exist (local, Docker, BYO R2, or
29
+ // Supabase-S3-with-keys). Only fall back to native Supabase Storage when there are no
30
+ // S3 keys but a Supabase project IS connected (service key + URL) — the Vercel case.
31
+ const hasS3Keys = Boolean(
32
+ process.env.R2_ACCESS_KEY_ID && process.env.R2_SECRET_ACCESS_KEY,
33
+ );
34
+ if (hasS3Keys) return 's3';
35
+ if (resolveSupabaseServiceKey() && resolveSupabaseUrl()) return 'supabase';
36
+ return 's3';
37
+ }
38
+
39
+ /** The bucket name to read/write. Defaults to "media" on the native Supabase backend. */
40
+ export function getStorageBucket(): string {
41
+ const explicit = process.env.STORAGE_BUCKET || process.env.R2_BUCKET_NAME;
42
+ if (explicit) return explicit;
43
+ return getStorageBackend() === 'supabase' ? 'media' : '';
44
+ }
45
+
46
+ /**
47
+ * The public base URL objects are served from — what resolveMediaUrl() joins object keys
48
+ * onto. An explicit R2 public/base URL always wins; otherwise, on the native Supabase
49
+ * backend, derive Supabase Storage's public object endpoint
50
+ * (`<project>/storage/v1/object/public/<bucket>`) so every display call site works with
51
+ * zero extra config. Kept in sync with the CJS computation in next.config.js.
52
+ */
53
+ export function resolveMediaBaseUrl(): string {
54
+ const explicit =
55
+ process.env.NEXT_PUBLIC_R2_BASE_URL || process.env.NEXT_PUBLIC_R2_PUBLIC_URL;
56
+ if (explicit) return explicit.replace(/\/+$/, '');
57
+
58
+ if (getStorageBackend() === 'supabase') {
59
+ const url = resolveSupabaseUrl();
60
+ const bucket = getStorageBucket();
61
+ if (url && bucket) {
62
+ return `${url.replace(/\/+$/, '')}/storage/v1/object/public/${bucket}`;
63
+ }
64
+ }
65
+ return '';
66
+ }
@@ -1,103 +1,103 @@
1
- import 'server-only';
2
-
3
- import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
4
-
5
- import { getStorageBucket } from './provider';
6
-
7
- // Native Supabase Storage operations, authenticated with the service-role/secret key.
8
- // Used only when getStorageBackend() === 'supabase' (the zero-S3-key Vercel path). These
9
- // mirror what the S3 routes do via @aws-sdk/client-s3, so the upload → process → display
10
- // → delete pipeline works identically without any S3 access keys.
11
-
12
- // The public bucket is created once per process. Cache the in-flight promise (not a bare
13
- // boolean) so concurrent uploads share a SINGLE createBucket call instead of racing — and
14
- // clear it on failure so a later request can retry rather than inheriting a cached rejection.
15
- let bucketEnsurePromise: Promise<void> | null = null;
16
-
17
- /** Idempotently ensure a PUBLIC bucket exists for media. Safe to call concurrently. */
18
- export function ensureStorageBucket(): Promise<void> {
19
- if (bucketEnsurePromise) return bucketEnsurePromise;
20
-
21
- bucketEnsurePromise = (async () => {
22
- const bucket = getStorageBucket();
23
- if (!bucket) throw new Error('No storage bucket name is configured.');
24
-
25
- const admin = getServiceRoleSupabaseClient();
26
- // public: true is REQUIRED — media is served from /storage/v1/object/public/<bucket>,
27
- // which returns 401 for a private bucket. Do not drop this flag.
28
- const { error } = await admin.storage.createBucket(bucket, { public: true });
29
- if (error && !/already exists|duplicate|resource already exists/i.test(error.message)) {
30
- // Non-"exists" error — confirm via getBucket before treating it as fatal.
31
- const { data } = await admin.storage.getBucket(bucket);
32
- if (!data) {
33
- throw new Error(`Could not ensure storage bucket "${bucket}": ${error.message}`);
34
- }
35
- }
36
- })();
37
-
38
- // On failure, drop the cached promise so the next caller retries (don't cache a rejection).
39
- bucketEnsurePromise.catch(() => {
40
- bucketEnsurePromise = null;
41
- });
42
-
43
- return bucketEnsurePromise;
44
- }
45
-
46
- /** Upload (or overwrite) an object. */
47
- export async function supabaseUploadObject(
48
- key: string,
49
- body: Buffer,
50
- contentType: string,
51
- ): Promise<void> {
52
- await ensureStorageBucket();
53
- const admin = getServiceRoleSupabaseClient();
54
- const { error } = await admin.storage
55
- .from(getStorageBucket())
56
- .upload(key, body, { contentType, upsert: true });
57
- if (error) {
58
- throw new Error(`Supabase Storage upload failed for "${key}": ${error.message}`);
59
- }
60
- }
61
-
62
- /** Download an object as a Buffer (used by the image processor to fetch the original). */
63
- export async function supabaseDownloadObject(key: string): Promise<Buffer> {
64
- const admin = getServiceRoleSupabaseClient();
65
- const { data, error } = await admin.storage.from(getStorageBucket()).download(key);
66
- if (error || !data) {
67
- throw new Error(
68
- `Supabase Storage download failed for "${key}": ${error?.message ?? 'no data returned'}`,
69
- );
70
- }
71
- return Buffer.from(await data.arrayBuffer());
72
- }
73
-
74
- /**
75
- * Create a signed upload URL for a NEW object key. The browser PUTs the file bytes to the
76
- * returned URL directly (the same shape as an S3 presigned PUT), so large uploads never
77
- * pass through the serverless function. The token is embedded in the URL.
78
- */
79
- export async function supabaseCreateSignedUpload(
80
- key: string,
81
- ): Promise<{ signedUrl: string; token: string }> {
82
- await ensureStorageBucket();
83
- const admin = getServiceRoleSupabaseClient();
84
- const { data, error } = await admin.storage
85
- .from(getStorageBucket())
86
- .createSignedUploadUrl(key);
87
- if (error || !data?.signedUrl) {
88
- throw new Error(
89
- `Supabase Storage signed upload URL failed for "${key}": ${error?.message ?? 'no URL returned'}`,
90
- );
91
- }
92
- return { signedUrl: data.signedUrl, token: data.token };
93
- }
94
-
95
- /** Remove one or more objects. No-op for an empty list. */
96
- export async function supabaseRemoveObjects(keys: string[]): Promise<void> {
97
- if (keys.length === 0) return;
98
- const admin = getServiceRoleSupabaseClient();
99
- const { error } = await admin.storage.from(getStorageBucket()).remove(keys);
100
- if (error) {
101
- throw new Error(`Supabase Storage remove failed: ${error.message}`);
102
- }
103
- }
1
+ import 'server-only';
2
+
3
+ import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
4
+
5
+ import { getStorageBucket } from './provider';
6
+
7
+ // Native Supabase Storage operations, authenticated with the service-role/secret key.
8
+ // Used only when getStorageBackend() === 'supabase' (the zero-S3-key Vercel path). These
9
+ // mirror what the S3 routes do via @aws-sdk/client-s3, so the upload → process → display
10
+ // → delete pipeline works identically without any S3 access keys.
11
+
12
+ // The public bucket is created once per process. Cache the in-flight promise (not a bare
13
+ // boolean) so concurrent uploads share a SINGLE createBucket call instead of racing — and
14
+ // clear it on failure so a later request can retry rather than inheriting a cached rejection.
15
+ let bucketEnsurePromise: Promise<void> | null = null;
16
+
17
+ /** Idempotently ensure a PUBLIC bucket exists for media. Safe to call concurrently. */
18
+ export function ensureStorageBucket(): Promise<void> {
19
+ if (bucketEnsurePromise) return bucketEnsurePromise;
20
+
21
+ bucketEnsurePromise = (async () => {
22
+ const bucket = getStorageBucket();
23
+ if (!bucket) throw new Error('No storage bucket name is configured.');
24
+
25
+ const admin = getServiceRoleSupabaseClient();
26
+ // public: true is REQUIRED — media is served from /storage/v1/object/public/<bucket>,
27
+ // which returns 401 for a private bucket. Do not drop this flag.
28
+ const { error } = await admin.storage.createBucket(bucket, { public: true });
29
+ if (error && !/already exists|duplicate|resource already exists/i.test(error.message)) {
30
+ // Non-"exists" error — confirm via getBucket before treating it as fatal.
31
+ const { data } = await admin.storage.getBucket(bucket);
32
+ if (!data) {
33
+ throw new Error(`Could not ensure storage bucket "${bucket}": ${error.message}`);
34
+ }
35
+ }
36
+ })();
37
+
38
+ // On failure, drop the cached promise so the next caller retries (don't cache a rejection).
39
+ bucketEnsurePromise.catch(() => {
40
+ bucketEnsurePromise = null;
41
+ });
42
+
43
+ return bucketEnsurePromise;
44
+ }
45
+
46
+ /** Upload (or overwrite) an object. */
47
+ export async function supabaseUploadObject(
48
+ key: string,
49
+ body: Buffer,
50
+ contentType: string,
51
+ ): Promise<void> {
52
+ await ensureStorageBucket();
53
+ const admin = getServiceRoleSupabaseClient();
54
+ const { error } = await admin.storage
55
+ .from(getStorageBucket())
56
+ .upload(key, body, { contentType, upsert: true });
57
+ if (error) {
58
+ throw new Error(`Supabase Storage upload failed for "${key}": ${error.message}`);
59
+ }
60
+ }
61
+
62
+ /** Download an object as a Buffer (used by the image processor to fetch the original). */
63
+ export async function supabaseDownloadObject(key: string): Promise<Buffer> {
64
+ const admin = getServiceRoleSupabaseClient();
65
+ const { data, error } = await admin.storage.from(getStorageBucket()).download(key);
66
+ if (error || !data) {
67
+ throw new Error(
68
+ `Supabase Storage download failed for "${key}": ${error?.message ?? 'no data returned'}`,
69
+ );
70
+ }
71
+ return Buffer.from(await data.arrayBuffer());
72
+ }
73
+
74
+ /**
75
+ * Create a signed upload URL for a NEW object key. The browser PUTs the file bytes to the
76
+ * returned URL directly (the same shape as an S3 presigned PUT), so large uploads never
77
+ * pass through the serverless function. The token is embedded in the URL.
78
+ */
79
+ export async function supabaseCreateSignedUpload(
80
+ key: string,
81
+ ): Promise<{ signedUrl: string; token: string }> {
82
+ await ensureStorageBucket();
83
+ const admin = getServiceRoleSupabaseClient();
84
+ const { data, error } = await admin.storage
85
+ .from(getStorageBucket())
86
+ .createSignedUploadUrl(key);
87
+ if (error || !data?.signedUrl) {
88
+ throw new Error(
89
+ `Supabase Storage signed upload URL failed for "${key}": ${error?.message ?? 'no URL returned'}`,
90
+ );
91
+ }
92
+ return { signedUrl: data.signedUrl, token: data.token };
93
+ }
94
+
95
+ /** Remove one or more objects. No-op for an empty list. */
96
+ export async function supabaseRemoveObjects(keys: string[]): Promise<void> {
97
+ if (keys.length === 0) return;
98
+ const admin = getServiceRoleSupabaseClient();
99
+ const { error } = await admin.storage.from(getStorageBucket()).remove(keys);
100
+ if (error) {
101
+ throw new Error(`Supabase Storage remove failed: ${error.message}`);
102
+ }
103
+ }