@nextsparkjs/core 0.1.0-beta.188 → 0.1.0-beta.189

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 (412) hide show
  1. package/dist/components/api/keys/ApiKeyDisplay.js +19 -19
  2. package/dist/components/api/keys/CreateApiKeyDialog.js +21 -21
  3. package/dist/components/app/layouts/PublicNavbar.d.ts.map +1 -1
  4. package/dist/components/app/layouts/PublicNavbar.js +34 -16
  5. package/dist/components/app/misc/ThemeToggle.d.ts.map +1 -1
  6. package/dist/components/app/misc/ThemeToggle.js +3 -4
  7. package/dist/components/auth/SessionCookieRefresher.d.ts +5 -0
  8. package/dist/components/auth/SessionCookieRefresher.d.ts.map +1 -1
  9. package/dist/components/auth/SessionCookieRefresher.js +7 -1
  10. package/dist/components/auth/forms/LoginForm.d.ts.map +1 -1
  11. package/dist/components/auth/forms/LoginForm.js +49 -12
  12. package/dist/components/auth/forms/SignupForm.d.ts.map +1 -1
  13. package/dist/components/auth/forms/SignupForm.js +4 -3
  14. package/dist/components/dashboard/block-editor/block-settings-panel.js +1 -1
  15. package/dist/components/dashboard/block-editor/floating-block-toolbar.js +1 -1
  16. package/dist/components/dashboard/block-editor/sortable-block.js +1 -1
  17. package/dist/components/dashboard/layouts/Sidebar.d.ts.map +1 -1
  18. package/dist/components/dashboard/layouts/Sidebar.js +8 -6
  19. package/dist/components/dashboard/layouts/TopNavbar.d.ts.map +1 -1
  20. package/dist/components/dashboard/layouts/TopNavbar.js +51 -54
  21. package/dist/components/dashboard/misc/NotificationsDropdown.d.ts.map +1 -1
  22. package/dist/components/dashboard/misc/NotificationsDropdown.js +17 -15
  23. package/dist/components/dashboard/misc/SearchDropdown.d.ts.map +1 -1
  24. package/dist/components/dashboard/misc/SearchDropdown.js +19 -22
  25. package/dist/components/dashboard/mobile/MobileBottomNav.js +1 -1
  26. package/dist/components/dashboard/mobile/MobileTopBar.js +2 -2
  27. package/dist/components/dashboard/navigation/DynamicNavigation.js +5 -5
  28. package/dist/components/devtools/BlockDetailViewer.js +1 -1
  29. package/dist/components/devtools/BlocksViewer.js +1 -1
  30. package/dist/components/entities/EntityChildManager.js +5 -5
  31. package/dist/components/entities/EntityDetailPage.d.ts.map +1 -1
  32. package/dist/components/entities/EntityDetailPage.js +16 -15
  33. package/dist/components/entities/EntityFieldRenderer.d.ts.map +1 -1
  34. package/dist/components/entities/EntityFieldRenderer.js +12 -7
  35. package/dist/components/entities/EntityPageHeader.js +5 -5
  36. package/dist/components/entities/ErrorBoundary.d.ts.map +1 -1
  37. package/dist/components/entities/ErrorBoundary.js +20 -21
  38. package/dist/components/entities/wrappers/EntityDetailWrapper.js +5 -5
  39. package/dist/components/entities/wrappers/EntityFormWrapper.js +2 -2
  40. package/dist/components/entities/wrappers/EntityListWrapper.js +3 -3
  41. package/dist/components/permissions/NoPermission.js +10 -10
  42. package/dist/components/public/pageBuilder/PageRenderer.d.ts.map +1 -1
  43. package/dist/components/public/pageBuilder/PageRenderer.js +2 -1
  44. package/dist/components/settings/SettingsPageSkeleton.js +4 -4
  45. package/dist/components/settings/layouts/SettingsSidebar.js +6 -6
  46. package/dist/components/shared/MultiSelectFilter.d.ts +3 -1
  47. package/dist/components/shared/MultiSelectFilter.d.ts.map +1 -1
  48. package/dist/components/shared/MultiSelectFilter.js +7 -3
  49. package/dist/components/shared/SearchInput.d.ts +2 -0
  50. package/dist/components/shared/SearchInput.d.ts.map +1 -1
  51. package/dist/components/shared/SearchInput.js +64 -6
  52. package/dist/components/ui/address-input.js +18 -18
  53. package/dist/components/ui/audio-upload.js +9 -9
  54. package/dist/components/ui/combobox.d.ts.map +1 -1
  55. package/dist/components/ui/combobox.js +4 -4
  56. package/dist/components/ui/country-select.d.ts.map +1 -1
  57. package/dist/components/ui/country-select.js +3 -3
  58. package/dist/components/ui/currency-select.d.ts.map +1 -1
  59. package/dist/components/ui/currency-select.js +3 -3
  60. package/dist/components/ui/dynamic-icon.d.ts.map +1 -1
  61. package/dist/components/ui/dynamic-icon.js +3 -2
  62. package/dist/components/ui/file-upload.js +7 -7
  63. package/dist/components/ui/image-upload.js +8 -8
  64. package/dist/components/ui/multi-select.d.ts +2 -1
  65. package/dist/components/ui/multi-select.d.ts.map +1 -1
  66. package/dist/components/ui/multi-select.js +4 -3
  67. package/dist/components/ui/phone-input.d.ts.map +1 -1
  68. package/dist/components/ui/phone-input.js +3 -3
  69. package/dist/components/ui/relation-display.js +1 -1
  70. package/dist/components/ui/simple-relation-select.d.ts +2 -1
  71. package/dist/components/ui/simple-relation-select.d.ts.map +1 -1
  72. package/dist/components/ui/simple-relation-select.js +7 -6
  73. package/dist/components/ui/tags-input.d.ts.map +1 -1
  74. package/dist/components/ui/tags-input.js +1 -1
  75. package/dist/components/ui/timezone-select.d.ts.map +1 -1
  76. package/dist/components/ui/timezone-select.js +3 -3
  77. package/dist/components/ui/user-display.js +4 -4
  78. package/dist/components/ui/user-select.d.ts.map +1 -1
  79. package/dist/components/ui/user-select.js +9 -9
  80. package/dist/components/ui/video-upload.js +9 -9
  81. package/dist/contexts/SubscriptionContext.d.ts.map +1 -1
  82. package/dist/contexts/SubscriptionContext.js +5 -1
  83. package/dist/contexts/TeamContext.d.ts.map +1 -1
  84. package/dist/contexts/TeamContext.js +83 -26
  85. package/dist/emails/otp-verification.d.ts.map +1 -1
  86. package/dist/emails/otp-verification.js +3 -2
  87. package/dist/hooks/useAccountPreferencesSync.d.ts +13 -0
  88. package/dist/hooks/useAccountPreferencesSync.d.ts.map +1 -0
  89. package/dist/hooks/useAccountPreferencesSync.js +95 -0
  90. package/dist/hooks/useAuth.d.ts +202 -15
  91. package/dist/hooks/useAuth.d.ts.map +1 -1
  92. package/dist/hooks/useAuth.js +49 -13
  93. package/dist/hooks/useEntityConfig.js +2 -2
  94. package/dist/hooks/useEntitySearch.js +10 -10
  95. package/dist/hooks/useNotifications.js +12 -12
  96. package/dist/hooks/usePrefetch.d.ts.map +1 -1
  97. package/dist/hooks/usePrefetch.js +6 -2
  98. package/dist/hooks/useSearch.js +15 -15
  99. package/dist/hooks/useSessionCookieRefresh.d.ts +18 -1
  100. package/dist/hooks/useSessionCookieRefresh.d.ts.map +1 -1
  101. package/dist/hooks/useSessionCookieRefresh.js +17 -4
  102. package/dist/hooks/useSessionHint.d.ts +12 -0
  103. package/dist/hooks/useSessionHint.d.ts.map +1 -0
  104. package/dist/hooks/useSessionHint.js +15 -0
  105. package/dist/hooks/useTranslationPreload.js +6 -6
  106. package/dist/i18n.d.ts +13 -0
  107. package/dist/i18n.d.ts.map +1 -1
  108. package/dist/i18n.js +2 -27
  109. package/dist/lib/actions/entity.actions.d.ts.map +1 -1
  110. package/dist/lib/actions/entity.actions.js +3 -2
  111. package/dist/lib/api/auth/dual-auth.d.ts +4 -1
  112. package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
  113. package/dist/lib/api/auth/dual-auth.js +5 -16
  114. package/dist/lib/api/entities.js +33 -33
  115. package/dist/lib/api/keys.d.ts +27 -27
  116. package/dist/lib/api/keys.js +27 -27
  117. package/dist/lib/auth/callback-url.d.ts +11 -0
  118. package/dist/lib/auth/callback-url.d.ts.map +1 -0
  119. package/dist/lib/auth/callback-url.js +14 -0
  120. package/dist/lib/auth/otp-config.d.ts +55 -0
  121. package/dist/lib/auth/otp-config.d.ts.map +1 -0
  122. package/dist/lib/auth/otp-config.js +48 -0
  123. package/dist/lib/auth/registration-helpers.d.ts.map +1 -1
  124. package/dist/lib/auth/registration-helpers.js +3 -1
  125. package/dist/lib/auth/session-hint.d.ts +31 -0
  126. package/dist/lib/auth/session-hint.d.ts.map +1 -0
  127. package/dist/lib/auth/session-hint.js +37 -0
  128. package/dist/lib/auth.d.ts.map +1 -1
  129. package/dist/lib/auth.js +6 -4
  130. package/dist/lib/blocks/loader.d.ts +3 -11
  131. package/dist/lib/blocks/loader.d.ts.map +1 -1
  132. package/dist/lib/blocks/loader.js +1 -9
  133. package/dist/lib/blocks/loader.server.d.ts +18 -0
  134. package/dist/lib/blocks/loader.server.d.ts.map +1 -0
  135. package/dist/lib/blocks/loader.server.js +11 -0
  136. package/dist/lib/blocks/sanitize-html.d.ts +31 -0
  137. package/dist/lib/blocks/sanitize-html.d.ts.map +1 -0
  138. package/dist/lib/blocks/sanitize-html.js +60 -0
  139. package/dist/lib/config/config-sync.d.ts +1 -0
  140. package/dist/lib/config/config-sync.d.ts.map +1 -1
  141. package/dist/lib/config/config-sync.js +4 -1
  142. package/dist/lib/config/types.d.ts +48 -0
  143. package/dist/lib/config/types.d.ts.map +1 -1
  144. package/dist/lib/email/types.d.ts +6 -0
  145. package/dist/lib/email/types.d.ts.map +1 -1
  146. package/dist/lib/entities/serialization.d.ts.map +1 -1
  147. package/dist/lib/entities/serialization.js +3 -6
  148. package/dist/lib/icons.d.ts +12 -0
  149. package/dist/lib/icons.d.ts.map +1 -0
  150. package/dist/lib/icons.js +30 -0
  151. package/dist/lib/locale.d.ts +25 -1
  152. package/dist/lib/locale.d.ts.map +1 -1
  153. package/dist/lib/locale.js +49 -35
  154. package/dist/lib/mcp/engine.js +1 -1
  155. package/dist/lib/mcp/errors.js +17 -17
  156. package/dist/lib/mcp/schema-builder.js +16 -16
  157. package/dist/lib/mcp/tool-generator.js +18 -18
  158. package/dist/lib/search-highlighting.d.ts +7 -0
  159. package/dist/lib/search-highlighting.d.ts.map +1 -1
  160. package/dist/lib/search-highlighting.js +6 -2
  161. package/dist/lib/selectors/core-selectors.d.ts +2 -0
  162. package/dist/lib/selectors/core-selectors.d.ts.map +1 -1
  163. package/dist/lib/selectors/domains/auth.selectors.d.ts +1 -0
  164. package/dist/lib/selectors/domains/auth.selectors.d.ts.map +1 -1
  165. package/dist/lib/selectors/domains/auth.selectors.js +1 -0
  166. package/dist/lib/selectors/domains/entities.selectors.d.ts +2 -0
  167. package/dist/lib/selectors/domains/entities.selectors.d.ts.map +1 -1
  168. package/dist/lib/selectors/domains/entities.selectors.js +2 -0
  169. package/dist/lib/selectors/selectors.d.ts +4 -0
  170. package/dist/lib/selectors/selectors.d.ts.map +1 -1
  171. package/dist/lib/services/block.service.js +1 -1
  172. package/dist/lib/teams/active-team-cookie.d.ts +24 -0
  173. package/dist/lib/teams/active-team-cookie.d.ts.map +1 -0
  174. package/dist/lib/teams/active-team-cookie.js +17 -0
  175. package/dist/lib/teams/dashboard-team.d.ts +20 -0
  176. package/dist/lib/teams/dashboard-team.d.ts.map +1 -0
  177. package/dist/lib/teams/dashboard-team.js +38 -0
  178. package/dist/lib/teams/schema.d.ts.map +1 -1
  179. package/dist/lib/teams/schema.js +1 -1
  180. package/dist/lib/template-resolver.d.ts.map +1 -1
  181. package/dist/lib/template-resolver.js +0 -1
  182. package/dist/lib/theme/get-default-theme-mode.d.ts +7 -4
  183. package/dist/lib/theme/get-default-theme-mode.d.ts.map +1 -1
  184. package/dist/lib/theme/get-default-theme-mode.js +2 -44
  185. package/dist/messages/de/auth.json +4 -2
  186. package/dist/messages/de/common.json +83 -3
  187. package/dist/messages/de/email.json +1 -1
  188. package/dist/messages/de/index.d.ts +87 -0
  189. package/dist/messages/de/index.d.ts.map +1 -1
  190. package/dist/messages/de/settings.json +5 -0
  191. package/dist/messages/en/auth.json +4 -2
  192. package/dist/messages/en/common.json +83 -3
  193. package/dist/messages/en/email.json +1 -1
  194. package/dist/messages/en/index.d.ts +86 -0
  195. package/dist/messages/en/index.d.ts.map +1 -1
  196. package/dist/messages/en/settings.json +4 -0
  197. package/dist/messages/es/auth.json +4 -2
  198. package/dist/messages/es/common.json +83 -3
  199. package/dist/messages/es/email.json +1 -1
  200. package/dist/messages/es/index.d.ts +86 -0
  201. package/dist/messages/es/index.d.ts.map +1 -1
  202. package/dist/messages/es/settings.json +4 -0
  203. package/dist/messages/fr/auth.json +4 -2
  204. package/dist/messages/fr/common.json +83 -3
  205. package/dist/messages/fr/email.json +1 -1
  206. package/dist/messages/fr/index.d.ts +87 -0
  207. package/dist/messages/fr/index.d.ts.map +1 -1
  208. package/dist/messages/fr/settings.json +5 -0
  209. package/dist/messages/it/auth.json +4 -2
  210. package/dist/messages/it/common.json +83 -3
  211. package/dist/messages/it/email.json +1 -1
  212. package/dist/messages/it/index.d.ts +87 -0
  213. package/dist/messages/it/index.d.ts.map +1 -1
  214. package/dist/messages/it/settings.json +5 -0
  215. package/dist/messages/pt/auth.json +4 -2
  216. package/dist/messages/pt/common.json +83 -3
  217. package/dist/messages/pt/email.json +1 -1
  218. package/dist/messages/pt/index.d.ts +87 -0
  219. package/dist/messages/pt/index.d.ts.map +1 -1
  220. package/dist/messages/pt/settings.json +5 -0
  221. package/dist/nextspark-registries.d.ts +54 -0
  222. package/dist/providers/DashboardProviders.d.ts +13 -10
  223. package/dist/providers/DashboardProviders.d.ts.map +1 -1
  224. package/dist/providers/DashboardProviders.js +7 -1
  225. package/dist/templates/app/(auth)/forgot-password/page.tsx +2 -2
  226. package/dist/templates/app/(auth)/reset-password/page.tsx +6 -6
  227. package/dist/templates/app/403/page.tsx +6 -6
  228. package/dist/templates/app/api/v1/teams/switch/route.ts +24 -5
  229. package/dist/templates/app/dashboard/(main)/[entity]/layout.tsx +9 -7
  230. package/dist/templates/app/dashboard/(main)/layout.tsx +4 -3
  231. package/dist/templates/app/dashboard/settings/api-keys/page.tsx +36 -34
  232. package/dist/templates/app/dashboard/settings/profile/page.tsx +7 -6
  233. package/dist/templates/app/dashboard/settings/security/page.tsx +5 -4
  234. package/dist/templates/app/layout.ppr.tsx +8 -8
  235. package/dist/templates/app/layout.tsx +12 -17
  236. package/dist/templates/blocks/cta-section/component.tsx +5 -2
  237. package/dist/templates/blocks/cta-section/config.ts +3 -2
  238. package/dist/templates/blocks/cta-section/fields.ts +2 -2
  239. package/dist/templates/blocks/cta-section/schema.ts +2 -2
  240. package/dist/templates/blocks/features-grid/component.tsx +10 -2
  241. package/dist/templates/blocks/features-grid/config.ts +3 -2
  242. package/dist/templates/blocks/features-grid/fields.ts +2 -2
  243. package/dist/templates/blocks/features-grid/schema.ts +2 -2
  244. package/dist/templates/blocks/hero/component.tsx +6 -3
  245. package/dist/templates/blocks/hero/config.ts +3 -2
  246. package/dist/templates/blocks/hero/fields.ts +2 -2
  247. package/dist/templates/blocks/hero/schema.ts +2 -2
  248. package/dist/templates/blocks/testimonials/component.tsx +6 -3
  249. package/dist/templates/blocks/testimonials/config.ts +3 -2
  250. package/dist/templates/blocks/testimonials/fields.ts +2 -2
  251. package/dist/templates/blocks/testimonials/schema.ts +2 -2
  252. package/dist/templates/blocks/text-content/component.tsx +7 -3
  253. package/dist/templates/blocks/text-content/config.ts +3 -2
  254. package/dist/templates/blocks/text-content/fields.ts +2 -2
  255. package/dist/templates/blocks/text-content/schema.ts +2 -2
  256. package/dist/templates/contents/themes/starter/blocks/cta-section/component.tsx +102 -0
  257. package/dist/templates/contents/themes/starter/blocks/cta-section/config.ts +11 -0
  258. package/dist/templates/contents/themes/starter/blocks/cta-section/fields.ts +89 -0
  259. package/dist/templates/contents/themes/starter/blocks/cta-section/index.ts +6 -0
  260. package/dist/templates/contents/themes/starter/blocks/cta-section/schema.ts +32 -0
  261. package/dist/templates/contents/themes/starter/blocks/cta-section/thumbnail.png +0 -0
  262. package/dist/templates/contents/themes/starter/blocks/features-grid/component.tsx +119 -0
  263. package/dist/templates/contents/themes/starter/blocks/features-grid/config.ts +11 -0
  264. package/dist/templates/contents/themes/starter/blocks/features-grid/fields.ts +97 -0
  265. package/dist/templates/contents/themes/starter/blocks/features-grid/index.ts +6 -0
  266. package/dist/templates/contents/themes/starter/blocks/features-grid/schema.ts +40 -0
  267. package/dist/templates/contents/themes/starter/blocks/features-grid/thumbnail.png +0 -0
  268. package/dist/templates/contents/themes/starter/blocks/hero/component.tsx +105 -0
  269. package/dist/templates/contents/themes/starter/blocks/hero/config.ts +11 -0
  270. package/dist/templates/contents/themes/starter/blocks/hero/fields.ts +60 -0
  271. package/dist/templates/contents/themes/starter/blocks/hero/index.ts +6 -0
  272. package/dist/templates/contents/themes/starter/blocks/hero/schema.ts +33 -0
  273. package/dist/templates/contents/themes/starter/blocks/hero/thumbnail.png +0 -0
  274. package/dist/templates/contents/themes/starter/blocks/testimonials/component.tsx +116 -0
  275. package/dist/templates/contents/themes/starter/blocks/testimonials/config.ts +11 -0
  276. package/dist/templates/contents/themes/starter/blocks/testimonials/fields.ts +105 -0
  277. package/dist/templates/contents/themes/starter/blocks/testimonials/index.ts +6 -0
  278. package/dist/templates/contents/themes/starter/blocks/testimonials/schema.ts +41 -0
  279. package/dist/templates/contents/themes/starter/blocks/testimonials/thumbnail.png +0 -0
  280. package/dist/templates/contents/themes/starter/blocks/text-content/component.tsx +100 -0
  281. package/dist/templates/contents/themes/starter/blocks/text-content/config.ts +11 -0
  282. package/dist/templates/contents/themes/starter/blocks/text-content/fields.ts +88 -0
  283. package/dist/templates/contents/themes/starter/blocks/text-content/index.ts +6 -0
  284. package/dist/templates/contents/themes/starter/blocks/text-content/schema.ts +30 -0
  285. package/dist/templates/contents/themes/starter/blocks/text-content/thumbnail.png +0 -0
  286. package/dist/templates/contents/themes/starter/config/app.config.ts +8 -0
  287. package/dist/templates/contents/themes/starter/entities/tasks/tasks.config.ts +7 -0
  288. package/dist/templates/contents/themes/starter/tests/cypress/e2e/_utils/devtools/access.cy.ts +9 -1
  289. package/dist/templates/contents/themes/starter/tests/cypress/e2e/api/entities/tasks/TaskAPIController.js +2 -2
  290. package/dist/templates/contents/themes/starter/tests/cypress/e2e/uat/entities/tasks/tasks-crud.cy.ts +45 -38
  291. package/dist/templates/contents/themes/starter/tests/cypress/fixtures/entities.json +16 -0
  292. package/dist/templates/contents/themes/starter/tests/cypress/src/components/DevKeyringPOM.ts +3 -1
  293. package/dist/templates/contents/themes/starter/tests/cypress/src/core/BlockEditorBasePOM.ts +3 -3
  294. package/dist/templates/contents/themes/starter/tests/cypress/src/entities/PagesPOM.ts +137 -0
  295. package/dist/templates/contents/themes/starter/tests/cypress/src/entities/index.ts +2 -0
  296. package/dist/templates/contents/themes/starter/tests/cypress/src/index.ts +1 -0
  297. package/dist/templates/contents/themes/starter/tests/cypress/src/session-helpers.ts +42 -11
  298. package/dist/templates/contents/themes/starter/tests/cypress/support/commands.ts +29 -18
  299. package/dist/templates/features/blog/blocks/post-content/component.tsx +5 -1
  300. package/dist/templates/features/blog/blocks/post-content/schema.ts +1 -1
  301. package/dist/templates/features/pages/blocks/hero/component.tsx +3 -0
  302. package/dist/templates/features/pages/blocks/hero/schema.ts +1 -1
  303. package/dist/templates/proxy.ts +138 -53
  304. package/dist/types/blocks.d.ts.map +1 -1
  305. package/dist/types/blocks.js +1 -1
  306. package/dist/types/task.types.d.ts +1 -1
  307. package/dist/types/task.types.js +7 -7
  308. package/dist/utils/dev/TranslationDebugger.js +2 -2
  309. package/package.json +14 -5
  310. package/scripts/build/registry/__tests__/block-registry.test.mjs +73 -0
  311. package/scripts/build/registry/__tests__/icons-discovery.test.mjs +130 -0
  312. package/scripts/build/registry/__tests__/identifier.test.mjs +65 -0
  313. package/scripts/build/registry/__tests__/page-generator.test.mjs +430 -0
  314. package/scripts/build/registry/__tests__/scheduled-actions-registry.test.mjs +41 -0
  315. package/scripts/build/registry/__tests__/template-registry.test.mjs +89 -0
  316. package/scripts/build/registry/discovery/icons.mjs +275 -0
  317. package/scripts/build/registry/discovery/index.mjs +1 -0
  318. package/scripts/build/registry/generators/block-registry.mjs +203 -55
  319. package/scripts/build/registry/generators/feature-registry.mjs +9 -2
  320. package/scripts/build/registry/generators/icon-registry.mjs +56 -0
  321. package/scripts/build/registry/generators/identifier.mjs +28 -0
  322. package/scripts/build/registry/generators/index.mjs +1 -0
  323. package/scripts/build/registry/generators/plugin-registry.mjs +4 -3
  324. package/scripts/build/registry/generators/scheduled-actions-registry.mjs +4 -2
  325. package/scripts/build/registry/generators/template-registry.mjs +49 -41
  326. package/scripts/build/registry/generators/theme-registry.mjs +6 -7
  327. package/scripts/build/registry/post-build/page-generator.mjs +380 -59
  328. package/scripts/build/registry.mjs +9 -1
  329. package/templates/app/(auth)/forgot-password/page.tsx +2 -2
  330. package/templates/app/(auth)/reset-password/page.tsx +6 -6
  331. package/templates/app/403/page.tsx +6 -6
  332. package/templates/app/api/v1/teams/switch/route.ts +24 -5
  333. package/templates/app/dashboard/(main)/[entity]/layout.tsx +9 -7
  334. package/templates/app/dashboard/(main)/layout.tsx +4 -3
  335. package/templates/app/dashboard/settings/api-keys/page.tsx +36 -34
  336. package/templates/app/dashboard/settings/profile/page.tsx +7 -6
  337. package/templates/app/dashboard/settings/security/page.tsx +5 -4
  338. package/templates/app/layout.ppr.tsx +8 -8
  339. package/templates/app/layout.tsx +12 -17
  340. package/templates/blocks/cta-section/component.tsx +5 -2
  341. package/templates/blocks/cta-section/config.ts +3 -2
  342. package/templates/blocks/cta-section/fields.ts +2 -2
  343. package/templates/blocks/cta-section/schema.ts +2 -2
  344. package/templates/blocks/features-grid/component.tsx +10 -2
  345. package/templates/blocks/features-grid/config.ts +3 -2
  346. package/templates/blocks/features-grid/fields.ts +2 -2
  347. package/templates/blocks/features-grid/schema.ts +2 -2
  348. package/templates/blocks/hero/component.tsx +6 -3
  349. package/templates/blocks/hero/config.ts +3 -2
  350. package/templates/blocks/hero/fields.ts +2 -2
  351. package/templates/blocks/hero/schema.ts +2 -2
  352. package/templates/blocks/testimonials/component.tsx +6 -3
  353. package/templates/blocks/testimonials/config.ts +3 -2
  354. package/templates/blocks/testimonials/fields.ts +2 -2
  355. package/templates/blocks/testimonials/schema.ts +2 -2
  356. package/templates/blocks/text-content/component.tsx +7 -3
  357. package/templates/blocks/text-content/config.ts +3 -2
  358. package/templates/blocks/text-content/fields.ts +2 -2
  359. package/templates/blocks/text-content/schema.ts +2 -2
  360. package/templates/contents/themes/starter/blocks/cta-section/component.tsx +102 -0
  361. package/templates/contents/themes/starter/blocks/cta-section/config.ts +11 -0
  362. package/templates/contents/themes/starter/blocks/cta-section/fields.ts +89 -0
  363. package/templates/contents/themes/starter/blocks/cta-section/index.ts +6 -0
  364. package/templates/contents/themes/starter/blocks/cta-section/schema.ts +32 -0
  365. package/templates/contents/themes/starter/blocks/cta-section/thumbnail.png +0 -0
  366. package/templates/contents/themes/starter/blocks/features-grid/component.tsx +119 -0
  367. package/templates/contents/themes/starter/blocks/features-grid/config.ts +11 -0
  368. package/templates/contents/themes/starter/blocks/features-grid/fields.ts +97 -0
  369. package/templates/contents/themes/starter/blocks/features-grid/index.ts +6 -0
  370. package/templates/contents/themes/starter/blocks/features-grid/schema.ts +40 -0
  371. package/templates/contents/themes/starter/blocks/features-grid/thumbnail.png +0 -0
  372. package/templates/contents/themes/starter/blocks/hero/component.tsx +105 -0
  373. package/templates/contents/themes/starter/blocks/hero/config.ts +11 -0
  374. package/templates/contents/themes/starter/blocks/hero/fields.ts +60 -0
  375. package/templates/contents/themes/starter/blocks/hero/index.ts +6 -0
  376. package/templates/contents/themes/starter/blocks/hero/schema.ts +33 -0
  377. package/templates/contents/themes/starter/blocks/hero/thumbnail.png +0 -0
  378. package/templates/contents/themes/starter/blocks/testimonials/component.tsx +116 -0
  379. package/templates/contents/themes/starter/blocks/testimonials/config.ts +11 -0
  380. package/templates/contents/themes/starter/blocks/testimonials/fields.ts +105 -0
  381. package/templates/contents/themes/starter/blocks/testimonials/index.ts +6 -0
  382. package/templates/contents/themes/starter/blocks/testimonials/schema.ts +41 -0
  383. package/templates/contents/themes/starter/blocks/testimonials/thumbnail.png +0 -0
  384. package/templates/contents/themes/starter/blocks/text-content/component.tsx +100 -0
  385. package/templates/contents/themes/starter/blocks/text-content/config.ts +11 -0
  386. package/templates/contents/themes/starter/blocks/text-content/fields.ts +88 -0
  387. package/templates/contents/themes/starter/blocks/text-content/index.ts +6 -0
  388. package/templates/contents/themes/starter/blocks/text-content/schema.ts +30 -0
  389. package/templates/contents/themes/starter/blocks/text-content/thumbnail.png +0 -0
  390. package/templates/contents/themes/starter/config/app.config.ts +8 -0
  391. package/templates/contents/themes/starter/entities/tasks/tasks.config.ts +7 -0
  392. package/templates/contents/themes/starter/tests/cypress/e2e/_utils/devtools/access.cy.ts +9 -1
  393. package/templates/contents/themes/starter/tests/cypress/e2e/api/entities/tasks/TaskAPIController.js +2 -2
  394. package/templates/contents/themes/starter/tests/cypress/e2e/uat/entities/tasks/tasks-crud.cy.ts +45 -38
  395. package/templates/contents/themes/starter/tests/cypress/fixtures/entities.json +16 -0
  396. package/templates/contents/themes/starter/tests/cypress/src/components/DevKeyringPOM.ts +3 -1
  397. package/templates/contents/themes/starter/tests/cypress/src/core/BlockEditorBasePOM.ts +3 -3
  398. package/templates/contents/themes/starter/tests/cypress/src/entities/PagesPOM.ts +137 -0
  399. package/templates/contents/themes/starter/tests/cypress/src/entities/index.ts +2 -0
  400. package/templates/contents/themes/starter/tests/cypress/src/index.ts +1 -0
  401. package/templates/contents/themes/starter/tests/cypress/src/session-helpers.ts +42 -11
  402. package/templates/contents/themes/starter/tests/cypress/support/commands.ts +29 -18
  403. package/templates/features/blog/blocks/post-content/component.tsx +5 -1
  404. package/templates/features/blog/blocks/post-content/schema.ts +1 -1
  405. package/templates/features/pages/blocks/hero/component.tsx +3 -0
  406. package/templates/features/pages/blocks/hero/schema.ts +1 -1
  407. package/templates/proxy.ts +138 -53
  408. package/tests/jest/__mocks__/@nextsparkjs/registries/block-registry.client.ts +9 -0
  409. package/tests/jest/__mocks__/@nextsparkjs/registries/block-registry.lazy.ts +5 -0
  410. package/tests/jest/__mocks__/@nextsparkjs/registries/icon-registry.ts +42 -0
  411. package/tests/jest/__mocks__/next-intl-server.js +85 -5
  412. package/tests/jest/__mocks__/next-server.js +32 -0
@@ -85,4 +85,5 @@ export {
85
85
  loginAsViewer,
86
86
  loginAsDefaultDeveloper,
87
87
  loginAsDefaultSuperadmin,
88
+ hasSeededTeamRoleUser,
88
89
  } from './session-helpers'
@@ -39,19 +39,50 @@ const SUPERADMIN_PASSWORD = Cypress.env('SUPERADMIN_PASSWORD') || 'Pandora1234'
39
39
  const TEST_PASSWORD = Cypress.env('TEST_PASSWORD') || 'Test1234'
40
40
 
41
41
  /**
42
- * Default Theme Test Users
43
- * Teams: Everpoint Labs, Ironvale Global, Riverstone Ventures
42
+ * Team-role test users.
44
43
  *
45
- * Note: These are fallback demo users. For selector tests, use CORE_USER (developer).
44
+ * The starter seeds exactly two users (core migration 090): superadmin and
45
+ * developer. The names below come from the default theme's richer sample data,
46
+ * and every one of them has to be pointed at a real user through the matching
47
+ * env var before the specs that use it can run.
48
+ *
49
+ * OWNER is the exception, and it matters: it is what the CRUD specs log in as,
50
+ * so its fallback is the user the starter actually creates — superadmin owns
51
+ * the initial team. Leaving it on a name with no row made the whole UAT suite
52
+ * fail in session setup, before a single assertion.
46
53
  */
47
54
  export const DEFAULT_THEME_USERS = {
48
- OWNER: Cypress.env('OWNER_EMAIL') || 'carlos.mendoza@nextspark.dev',
55
+ OWNER: Cypress.env('OWNER_EMAIL') || 'superadmin@nextspark.dev',
49
56
  ADMIN: Cypress.env('ADMIN_EMAIL') || 'james.wilson@nextspark.dev',
50
57
  MEMBER: Cypress.env('MEMBER_EMAIL') || 'emily.johnson@nextspark.dev',
51
58
  EDITOR: Cypress.env('EDITOR_EMAIL') || 'diego.ramirez@nextspark.dev',
52
59
  VIEWER: Cypress.env('VIEWER_EMAIL') || 'sarah.davis@nextspark.dev',
53
60
  } as const
54
61
 
62
+ /**
63
+ * The password that goes with OWNER.
64
+ *
65
+ * OWNER falls back to the seeded superadmin, and that user's password is the
66
+ * superadmin one, not the demo-user one. A theme that points OWNER at a user of
67
+ * its own sets OWNER_EMAIL, and then the demo password is the right default.
68
+ */
69
+ const OWNER_PASSWORD =
70
+ Cypress.env('OWNER_PASSWORD') ||
71
+ (Cypress.env('OWNER_EMAIL') ? TEST_PASSWORD : SUPERADMIN_PASSWORD)
72
+
73
+ /**
74
+ * Whether a non-owner team role points at a user that exists.
75
+ *
76
+ * Only OWNER has a seeded fallback. The other four keep the default theme's
77
+ * names, which resolve to nothing here until the theme seeds them and sets the
78
+ * matching env var. A spec that needs one guards on this instead of failing in
79
+ * session setup, where the error is a ten-second timeout on the Dev Keyring
80
+ * rather than "this user does not exist".
81
+ */
82
+ export function hasSeededTeamRoleUser(role: 'ADMIN' | 'MEMBER' | 'EDITOR' | 'VIEWER'): boolean {
83
+ return Boolean(Cypress.env(`${role}_EMAIL`))
84
+ }
85
+
55
86
  /**
56
87
  * Core System Users (from core/migrations/090_sample_data.sql)
57
88
  * These users have special global roles, not team-based roles
@@ -164,7 +195,7 @@ function apiLogin(email: string, password: string = TEST_PASSWORD): Cypress.Chai
164
195
  */
165
196
  export function loginAsDefaultOwner() {
166
197
  cy.session('default-owner-session', () => {
167
- apiLogin(DEFAULT_THEME_USERS.OWNER).then((apiLoginSucceeded) => {
198
+ apiLogin(DEFAULT_THEME_USERS.OWNER, OWNER_PASSWORD).then((apiLoginSucceeded) => {
168
199
  // If API login succeeded, we need to visit a page before setting localStorage
169
200
  if (apiLoginSucceeded) {
170
201
  cy.visit('/dashboard', { timeout: 60000 })
@@ -389,22 +420,22 @@ export function getThemeUsers() {
389
420
  */
390
421
  export const BILLING_TEAMS = {
391
422
  FREE: {
392
- teamId: 'team-personal-carlos-001',
423
+ teamId: Cypress.env('BILLING_FREE_TEAM_ID') || 'team-personal-carlos-001',
393
424
  name: 'Carlos Personal',
394
425
  planSlug: 'free',
395
- owner: 'carlos.mendoza@nextspark.dev'
426
+ owner: Cypress.env('BILLING_FREE_OWNER') || 'carlos.mendoza@nextspark.dev'
396
427
  },
397
428
  PRO: {
398
- teamId: 'team-everpoint-001',
429
+ teamId: Cypress.env('BILLING_PRO_TEAM_ID') || 'team-everpoint-001',
399
430
  name: 'Everpoint Labs',
400
431
  planSlug: 'pro',
401
- owner: 'carlos.mendoza@nextspark.dev'
432
+ owner: Cypress.env('BILLING_PRO_OWNER') || 'carlos.mendoza@nextspark.dev'
402
433
  },
403
434
  ENTERPRISE: {
404
- teamId: 'team-ironvale-002',
435
+ teamId: Cypress.env('BILLING_ENTERPRISE_TEAM_ID') || 'team-ironvale-002',
405
436
  name: 'Ironvale Global',
406
437
  planSlug: 'enterprise',
407
- owner: 'ana.garcia@nextspark.dev'
438
+ owner: Cypress.env('BILLING_ENTERPRISE_OWNER') || 'ana.garcia@nextspark.dev'
408
439
  }
409
440
  } as const
410
441
 
@@ -140,18 +140,37 @@ Cypress.Commands.add('logout', () => {
140
140
  })
141
141
 
142
142
  /**
143
- * Create a task via API
143
+ * The headers an entity API call needs, `x-team-id` included.
144
+ *
145
+ * The app writes `activeTeamId` into localStorage while the dashboard loads, so
146
+ * a command that runs before any dashboard visit — a `beforeEach` seeding data,
147
+ * typically — finds nothing there. Without the header the API answers 400
148
+ * TEAM_CONTEXT_REQUIRED, and since these requests do not fail on status the
149
+ * seed silently does nothing: the failure only surfaces later, as an assertion
150
+ * about data that was never created. Asking the API for the team is what makes
151
+ * the command work from any starting point.
144
152
  */
145
- Cypress.Commands.add('createTask', (data) => {
153
+ function apiHeaders(): Cypress.Chainable<Record<string, string>> {
154
+ const base: Record<string, string> = { 'Content-Type': 'application/json' }
155
+
146
156
  return cy.window().then((win) => {
147
- const teamId = win.localStorage.getItem('activeTeamId')
148
- const headers: Record<string, string> = {
149
- 'Content-Type': 'application/json',
150
- }
151
- if (teamId) {
152
- headers['x-team-id'] = teamId
153
- }
157
+ const stored = win.localStorage.getItem('activeTeamId')
158
+ if (stored) return cy.wrap({ ...base, 'x-team-id': stored })
159
+
160
+ return cy
161
+ .request({ method: 'GET', url: '/api/v1/teams', failOnStatusCode: false })
162
+ .then((response) => {
163
+ const teamId = response.body?.data?.[0]?.id
164
+ return cy.wrap(teamId ? { ...base, 'x-team-id': teamId } : base)
165
+ })
166
+ })
167
+ }
154
168
 
169
+ /**
170
+ * Create a task via API
171
+ */
172
+ Cypress.Commands.add('createTask', (data) => {
173
+ return apiHeaders().then((headers) => {
155
174
  return cy.request({
156
175
  method: 'POST',
157
176
  url: '/api/v1/tasks',
@@ -171,15 +190,7 @@ Cypress.Commands.add('createTask', (data) => {
171
190
  * Delete a task via API
172
191
  */
173
192
  Cypress.Commands.add('deleteTask', (id: string) => {
174
- return cy.window().then((win) => {
175
- const teamId = win.localStorage.getItem('activeTeamId')
176
- const headers: Record<string, string> = {
177
- 'Content-Type': 'application/json',
178
- }
179
- if (teamId) {
180
- headers['x-team-id'] = teamId
181
- }
182
-
193
+ return apiHeaders().then((headers) => {
183
194
  return cy.request({
184
195
  method: 'DELETE',
185
196
  url: `/api/v1/tasks/${id}`,
@@ -1,6 +1,7 @@
1
1
  import React from 'react'
2
2
  import { cn } from '@nextsparkjs/core/lib/utils'
3
3
  import { buildSectionClasses } from '@nextsparkjs/core/types/blocks'
4
+ import { sanitizeBlockHtml } from '@nextsparkjs/core/lib/blocks/sanitize-html'
4
5
  import { sel } from '../../lib/selectors'
5
6
  import type { PostContentBlockProps } from './schema'
6
7
 
@@ -171,7 +172,7 @@ export function PostContentBlock({
171
172
  // Em/italic
172
173
  'prose-em:italic'
173
174
  )}
174
- dangerouslySetInnerHTML={{ __html: content }}
175
+ dangerouslySetInnerHTML={{ __html: sanitizeBlockHtml(content) }}
175
176
  />
176
177
 
177
178
  {/* Optional Divider after content */}
@@ -195,3 +196,6 @@ export function PostContentBlock({
195
196
  </section>
196
197
  )
197
198
  }
199
+
200
+ // The generated block registry imports this as the module's default.
201
+ export default PostContentBlock
@@ -41,6 +41,6 @@ export const postContentSpecificSchema = z.object({
41
41
  * Complete Post Content Block Schema
42
42
  * Note: postContentSpecificSchema.content overrides baseBlockSchema.content
43
43
  */
44
- export const schema = baseBlockSchema.merge(postContentSpecificSchema)
44
+ export const schema = baseBlockSchema.extend(postContentSpecificSchema.shape)
45
45
 
46
46
  export type PostContentBlockProps = z.infer<typeof schema>
@@ -113,3 +113,6 @@ export function HeroBlock({
113
113
  </section>
114
114
  )
115
115
  }
116
+
117
+ // The generated block registry imports this as the module's default.
118
+ export default HeroBlock
@@ -27,7 +27,7 @@ export const heroSpecificSchema = z.object({
27
27
  * Complete Hero Block Schema
28
28
  * Combines base fields + hero-specific fields
29
29
  */
30
- export const schema = baseBlockSchema.merge(heroSpecificSchema)
30
+ export const schema = baseBlockSchema.extend(heroSpecificSchema.shape)
31
31
 
32
32
  export type HeroBlockProps = z.infer<typeof schema>
33
33
 
@@ -1,4 +1,10 @@
1
1
  /**
2
+ * @nextspark-generated
3
+ *
4
+ * `nextspark sync:app` replaces this file while that tag is present, which is
5
+ * how a release ships changes to it. Delete the tag line to own the file: from
6
+ * then on sync reports it and leaves it alone.
7
+ *
2
8
  * NextSpark Proxy (Next.js 16+)
3
9
  *
4
10
  * Handles authentication, route protection, and user context injection.
@@ -7,8 +13,8 @@
7
13
  * Key responsibilities:
8
14
  * 1. Theme middleware override support
9
15
  * 2. Documentation access control
10
- * 3. Protected route authentication
11
- * 4. User header injection for downstream use (x-user-id, x-pathname)
16
+ * 3. Protected route authentication, and the roles /superadmin and /devtools need
17
+ * 4. User header injection for downstream use (x-user-id, x-pathname, x-active-team-id)
12
18
  *
13
19
  * IMPORTANT: The EntityPermissionLayout depends on x-user-id and x-pathname
14
20
  * headers being set here for server-side permission validation.
@@ -20,6 +26,8 @@ import {
20
26
  executeThemeMiddleware,
21
27
  getThemeAppConfig
22
28
  } from '@nextsparkjs/core/lib/middleware'
29
+ import { ACTIVE_TEAM_COOKIE, activeTeamIdForSession } from '@nextsparkjs/core/lib/teams/active-team-cookie'
30
+ import { SESSION_HINT_COOKIE, SESSION_HINT_MAX_AGE, hasSessionCookie } from '@nextsparkjs/core/lib/auth/session-hint'
23
31
 
24
32
  /**
25
33
  * Session type for proxy (inline definition)
@@ -64,6 +72,53 @@ function isPublicPath(pathname: string): boolean {
64
72
  )
65
73
  }
66
74
 
75
+ /**
76
+ * Whether `pathname` is `prefix` or a path under it: `/devtools/config` is,
77
+ * `/devtools-guide` and `/profile-photo.jpg` are not.
78
+ */
79
+ function isUnder(pathname: string, prefix: string): boolean {
80
+ return pathname === prefix || pathname.startsWith(`${prefix}/`)
81
+ }
82
+
83
+ /**
84
+ * A URL on this app at `pathname`, keeping the base path and locale the request
85
+ * came in with; one built from `request.url` would lose both.
86
+ */
87
+ function appUrl(request: NextRequest, pathname: string): NextRequest['nextUrl'] {
88
+ const url = request.nextUrl.clone()
89
+ url.pathname = pathname
90
+ url.search = ''
91
+ return url
92
+ }
93
+
94
+ /**
95
+ * Send a visitor without a session to login. LoginForm reads `callbackUrl` and
96
+ * returns them to the page they asked for, query included, once signed in.
97
+ */
98
+ function redirectToLogin(request: NextRequest): NextResponse {
99
+ const loginUrl = appUrl(request, '/login')
100
+ loginUrl.searchParams.set('callbackUrl', `${request.nextUrl.pathname}${request.nextUrl.search}`)
101
+ return NextResponse.redirect(loginUrl)
102
+ }
103
+
104
+ /** Send a signed-in user without the role an area needs back to the dashboard. */
105
+ function redirectAccessDenied(request: NextRequest): NextResponse {
106
+ const dashboardUrl = appUrl(request, '/dashboard')
107
+ dashboardUrl.searchParams.set('error', 'access_denied')
108
+ return NextResponse.redirect(dashboardUrl)
109
+ }
110
+
111
+ /**
112
+ * The session the request's cookies carry, asked of this app's own auth route,
113
+ * which is served under the base path like every other route.
114
+ */
115
+ function getSession(request: NextRequest) {
116
+ return betterFetch<Session>('/api/auth/get-session', {
117
+ baseURL: `${request.nextUrl.origin}${request.nextUrl.basePath}`,
118
+ headers: { cookie: request.headers.get('cookie') || '' },
119
+ })
120
+ }
121
+
67
122
  /**
68
123
  * Identity headers this proxy injects for downstream server code.
69
124
  *
@@ -74,7 +129,7 @@ function isPublicPath(pathname: string): boolean {
74
129
  * dispatches Server Actions by the `Next-Action` header, not by the URL, so an
75
130
  * action can be POSTed to a public path with a forged `x-user-id`.
76
131
  */
77
- const TRUSTED_IDENTITY_HEADERS = ['x-user-id', 'x-user-email', 'x-pathname'] as const
132
+ const TRUSTED_IDENTITY_HEADERS = ['x-user-id', 'x-user-email', 'x-pathname', 'x-active-team-id'] as const
78
133
 
79
134
  /**
80
135
  * Build the request headers forwarded to the app: inbound copy minus every
@@ -89,15 +144,38 @@ function sanitizeRequestHeaders(request: NextRequest): Headers {
89
144
  return headers
90
145
  }
91
146
 
147
+ /**
148
+ * Keep the readable session hint (lib/auth/session-hint) in line with the
149
+ * session cookie, so client code knows whether to ask for the session. A browser
150
+ * that got its session without client code seeing it (before the hint existed,
151
+ * or through OAuth) gets the hint on its next request, and one whose session
152
+ * cookie is gone loses it. The session itself is not checked here.
153
+ */
154
+ function syncSessionHint(request: NextRequest, response: NextResponse): NextResponse {
155
+ const hasSession = hasSessionCookie(request.headers.get('cookie'))
156
+ const hinted = request.cookies.get(SESSION_HINT_COOKIE)?.value === '1'
157
+ if (hasSession && !hinted) {
158
+ response.cookies.set(SESSION_HINT_COOKIE, '1', {
159
+ path: '/',
160
+ maxAge: SESSION_HINT_MAX_AGE,
161
+ sameSite: 'lax',
162
+ secure: request.nextUrl.protocol === 'https:',
163
+ })
164
+ } else if (!hasSession && hinted) {
165
+ response.cookies.set(SESSION_HINT_COOKIE, '', { path: '/', maxAge: 0 })
166
+ }
167
+ return response
168
+ }
169
+
92
170
  /**
93
171
  * Continue to the app with the (sanitized) request headers.
94
172
  * Every pass-through in this proxy MUST go through here so the strip applies
95
173
  * to public paths, /api/v1 and unmatched routes alike.
96
174
  */
97
- function passThrough(requestHeaders: Headers): NextResponse {
98
- return NextResponse.next({
175
+ function passThrough(request: NextRequest, requestHeaders: Headers): NextResponse {
176
+ return syncSessionHint(request, NextResponse.next({
99
177
  request: { headers: requestHeaders },
100
- })
178
+ }))
101
179
  }
102
180
 
103
181
  export async function proxy(request: NextRequest) {
@@ -129,76 +207,71 @@ export async function proxy(request: NextRequest) {
129
207
  }
130
208
 
131
209
  // 3. Documentation access control
132
- if (pathname.startsWith('/docs')) {
210
+ if (isUnder(pathname, '/docs')) {
133
211
  const appConfig = getThemeAppConfig(activeTheme as string)
134
212
 
135
213
  if (appConfig?.docs?.public === false) {
136
214
  try {
137
- const { data: session } = await betterFetch<Session>(
138
- '/api/auth/get-session',
139
- {
140
- baseURL: request.nextUrl.origin,
141
- headers: { cookie: request.headers.get('cookie') || '' },
142
- }
143
- )
215
+ const { data: session } = await getSession(request)
144
216
 
145
217
  if (!session) {
146
- const loginUrl = new URL('/login', request.url)
147
- loginUrl.searchParams.set('redirect', pathname)
148
- return NextResponse.redirect(loginUrl)
218
+ return redirectToLogin(request)
149
219
  }
150
220
  } catch (error) {
151
- const loginUrl = new URL('/login', request.url)
152
- loginUrl.searchParams.set('redirect', pathname)
153
- return NextResponse.redirect(loginUrl)
221
+ return redirectToLogin(request)
154
222
  }
155
223
  }
156
- return passThrough(requestHeaders)
224
+ return passThrough(request, requestHeaders)
157
225
  }
158
226
 
159
227
  // 4. Allow public paths
160
228
  if (isPublicPath(pathname)) {
161
- return passThrough(requestHeaders)
229
+ return passThrough(request, requestHeaders)
162
230
  }
163
231
 
164
232
  // 5. API v1 routes handle their own dual authentication
165
233
  if (pathname.startsWith('/api/v1')) {
166
- return passThrough(requestHeaders)
234
+ return passThrough(request, requestHeaders)
167
235
  }
168
236
 
169
- // 6. Protected routes - require authentication and inject user headers
170
- const isAdminRoute = pathname.startsWith('/admin')
237
+ // 6. Protected routes - require authentication and inject user headers.
238
+ // Areas are matched by path segment, so /dashboard-guide is not /dashboard.
239
+ // /superadmin and /devtools also need a role, the same ones SuperAdminGuard
240
+ // and DeveloperGuard let in. The guards decide it again in the browser, but
241
+ // only after the page has been served, so it is decided here first.
242
+ const isAdminRoute = isUnder(pathname, '/admin')
243
+ const isSuperadminRoute = isUnder(pathname, '/superadmin')
244
+ const isDevtoolsRoute = isUnder(pathname, '/devtools')
171
245
  const isProtectedRoute =
172
- pathname.startsWith('/dashboard') ||
173
- pathname.startsWith('/admin') ||
174
- pathname.startsWith('/settings') ||
175
- pathname.startsWith('/profile') ||
176
- pathname.startsWith('/update-password') ||
177
- isAdminRoute
246
+ isUnder(pathname, '/dashboard') ||
247
+ isUnder(pathname, '/settings') ||
248
+ isUnder(pathname, '/profile') ||
249
+ isUnder(pathname, '/update-password') ||
250
+ isAdminRoute ||
251
+ isSuperadminRoute ||
252
+ isDevtoolsRoute
178
253
 
179
254
  if (isProtectedRoute) {
180
255
  try {
181
- const { data: session } = await betterFetch<Session>(
182
- '/api/auth/get-session',
183
- {
184
- baseURL: request.nextUrl.origin,
185
- headers: { cookie: request.headers.get('cookie') || '' },
186
- }
187
- )
256
+ const { data: session } = await getSession(request)
188
257
 
189
258
  if (!session) {
190
- const loginUrl = new URL('/login', request.url)
191
- loginUrl.searchParams.set('callbackUrl', pathname)
192
- return NextResponse.redirect(loginUrl)
259
+ return redirectToLogin(request)
193
260
  }
194
261
 
262
+ const role = session.user?.role
263
+
195
264
  // Admin Panel superadmin-only check
196
- if (isAdminRoute) {
197
- if (!session.user?.role || session.user.role !== 'superadmin') {
198
- const dashboardUrl = new URL('/dashboard', request.url)
199
- dashboardUrl.searchParams.set('error', 'access_denied')
200
- return NextResponse.redirect(dashboardUrl)
201
- }
265
+ if (isAdminRoute && role !== 'superadmin') {
266
+ return redirectAccessDenied(request)
267
+ }
268
+
269
+ if (isSuperadminRoute && role !== 'superadmin' && role !== 'developer') {
270
+ return redirectAccessDenied(request)
271
+ }
272
+
273
+ if (isDevtoolsRoute && role !== 'developer') {
274
+ return redirectAccessDenied(request)
202
275
  }
203
276
 
204
277
  // Inject user headers for downstream use, ONLY from the verified session
@@ -210,21 +283,33 @@ export async function proxy(request: NextRequest) {
210
283
  if (session.user?.email) {
211
284
  requestHeaders.set('x-user-email', session.user.email)
212
285
  }
286
+ // The team this session chose, for the dashboard layouts' permission
287
+ // checks; a cookie another session left behind names no team here.
288
+ const activeTeamId = activeTeamIdForSession(request.cookies.get(ACTIVE_TEAM_COOKIE)?.value, session.session?.id)
289
+ if (activeTeamId) {
290
+ requestHeaders.set('x-active-team-id', activeTeamId)
291
+ }
213
292
 
214
- return passThrough(requestHeaders)
293
+ return passThrough(request, requestHeaders)
215
294
  } catch (error) {
216
295
  console.error('Proxy error:', error)
217
- const loginUrl = new URL('/login', request.url)
218
- loginUrl.searchParams.set('callbackUrl', pathname)
219
- return NextResponse.redirect(loginUrl)
296
+ return redirectToLogin(request)
220
297
  }
221
298
  }
222
299
 
223
- return passThrough(requestHeaders)
300
+ return passThrough(request, requestHeaders)
224
301
  }
225
302
 
303
+ /**
304
+ * Everything runs through the proxy except the exact paths of Next's own
305
+ * output: `_next/static/`, the `_next/image` endpoint and `favicon.ico`.
306
+ * Excluding by file extension, or by a prefix without its boundary, would let a
307
+ * dynamic segment such as `alice.png` or `favicon.icoevil` reach the app without
308
+ * the session check and with forged identity headers intact. Files under
309
+ * public/ simply pass through.
310
+ */
226
311
  export const config = {
227
312
  matcher: [
228
- '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
313
+ '/((?!_next/static/|_next/image$|favicon\\.ico$).*)',
229
314
  ],
230
315
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The client half of the block registry, for tests.
3
+ *
4
+ * Re-exports the mock's configs rather than restating them, so the two halves
5
+ * cannot drift the way the generated pair must not.
6
+ */
7
+ export { BLOCK_REGISTRY, BLOCK_METADATA } from './block-registry'
8
+
9
+ export const BLOCK_CATEGORIES: string[] = []
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Mock Block Registry (lazy components) for Jest tests
3
+ */
4
+
5
+ export const BLOCK_COMPONENTS: Record<string, any> = {}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Mock Icon Registry for Jest tests
3
+ *
4
+ * Holds the real lucide components, not stand-ins: serialization resolves an
5
+ * icon's registry name by identity, so a config's `icon: CheckSquare` only
6
+ * round-trips when the registry holds that same component.
7
+ */
8
+ import type { LucideIcon } from 'lucide-react'
9
+ import {
10
+ Box,
11
+ CheckSquare,
12
+ Circle,
13
+ FileText,
14
+ Folder,
15
+ Home,
16
+ LayoutGrid,
17
+ Newspaper,
18
+ Search,
19
+ Settings,
20
+ Users
21
+ } from 'lucide-react'
22
+
23
+ export const ICON_REGISTRY: Record<string, LucideIcon> = {
24
+ Box,
25
+ CheckSquare,
26
+ Circle,
27
+ FileText,
28
+ Folder,
29
+ Home,
30
+ LayoutGrid,
31
+ Newspaper,
32
+ Search,
33
+ Settings,
34
+ Users
35
+ }
36
+
37
+ export type IconName = keyof typeof ICON_REGISTRY
38
+
39
+ export const ICON_METADATA = {
40
+ totalIcons: Object.keys(ICON_REGISTRY).length,
41
+ generatedAt: new Date().toISOString()
42
+ }
@@ -3,8 +3,10 @@
3
3
  *
4
4
  * Loads the corresponding `messages/<locale>/<root>.json` file directly and
5
5
  * resolves keys inside `namespace` (which can be dotted, e.g. `email.verifyEmail`).
6
- * Supports ICU-style `{var}` placeholders in values — replaced naively, no
7
- * pluralization. Sufficient for email template snapshot tests.
6
+ * Supports ICU-style `{var}` placeholders, replaced naively, plus the `plural`
7
+ * argument type (`{var, plural, one {...} other {...}}`) resolved through
8
+ * `Intl.PluralRules` — the subset the email templates actually use. Sufficient
9
+ * for email template snapshot tests, not a general ICU MessageFormat parser.
8
10
  */
9
11
 
10
12
  const path = require('path')
@@ -28,9 +30,87 @@ function loadNamespace(locale, namespace) {
28
30
  return obj
29
31
  }
30
32
 
31
- function format(template, params) {
33
+ /**
34
+ * Resolves every `{varName, plural, category {text} ...}` block in `template`,
35
+ * picking the branch via `Intl.PluralRules` (falling back to an explicit
36
+ * `=<n>` match, then to `other`) and substituting `#` with the value.
37
+ * Parses by brace-depth rather than regex so a category's text may itself
38
+ * contain other `{placeholder}` tokens.
39
+ */
40
+ function resolveIcuPlural(template, params, locale) {
41
+ const marker = ', plural,'
42
+ let result = ''
43
+ let i = 0
44
+
45
+ while (i < template.length) {
46
+ if (template[i] !== '{') {
47
+ result += template[i]
48
+ i++
49
+ continue
50
+ }
51
+
52
+ const commaIndex = template.indexOf(',', i)
53
+ if (commaIndex === -1 || template.slice(commaIndex, commaIndex + marker.length) !== marker) {
54
+ result += template[i]
55
+ i++
56
+ continue
57
+ }
58
+
59
+ const varName = template.slice(i + 1, commaIndex).trim()
60
+ let cursor = commaIndex + marker.length
61
+ const categories = {}
62
+
63
+ while (cursor < template.length) {
64
+ while (/\s/.test(template[cursor])) cursor++
65
+ if (template[cursor] === '}') {
66
+ cursor++
67
+ break
68
+ }
69
+ const catStart = cursor
70
+ while (template[cursor] !== '{') cursor++
71
+ const catName = template.slice(catStart, cursor).trim()
72
+ cursor++ // skip the category's opening '{'
73
+ let subDepth = 1
74
+ const subStart = cursor
75
+ while (subDepth > 0) {
76
+ if (template[cursor] === '{') subDepth++
77
+ else if (template[cursor] === '}') subDepth--
78
+ if (subDepth > 0) cursor++
79
+ }
80
+ categories[catName] = template.slice(subStart, cursor)
81
+ cursor++ // skip the category's closing '}'
82
+ while (/\s/.test(template[cursor])) cursor++
83
+ if (template[cursor] === '}') {
84
+ cursor++
85
+ break
86
+ }
87
+ }
88
+
89
+ const value = params ? params[varName] : undefined
90
+ const numValue = Number(value)
91
+ let category = categories[`=${numValue}`] !== undefined ? `=${numValue}` : undefined
92
+ if (category === undefined) {
93
+ try {
94
+ category = new Intl.PluralRules(locale || 'en').select(numValue)
95
+ } catch {
96
+ category = 'other'
97
+ }
98
+ if (categories[category] === undefined) category = 'other'
99
+ }
100
+
101
+ result += (categories[category] ?? '').replace(/#/g, String(numValue))
102
+ i = cursor
103
+ }
104
+
105
+ return result
106
+ }
107
+
108
+ function format(template, params, locale) {
32
109
  if (!params) return template
33
- return template.replace(/\{(\w+)\}/g, (_, key) =>
110
+ const resolved = template.includes(', plural,')
111
+ ? resolveIcuPlural(template, params, locale)
112
+ : template
113
+ return resolved.replace(/\{(\w+)\}/g, (_, key) =>
34
114
  params[key] === undefined ? `{${key}}` : String(params[key]),
35
115
  )
36
116
  }
@@ -45,7 +125,7 @@ async function getTranslations(opts) {
45
125
  if (value === undefined) {
46
126
  throw new Error(`next-intl mock: missing key "${key}" in namespace "${namespace}"`)
47
127
  }
48
- return format(value, params)
128
+ return format(value, params, locale)
49
129
  }
50
130
  }
51
131