@nextsparkjs/core 0.1.0-beta.195 → 0.1.0-beta.197

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 (283) hide show
  1. package/README.md +2 -0
  2. package/dist/components/auth/forms/SignupForm.d.ts.map +1 -1
  3. package/dist/components/auth/forms/SignupForm.js +55 -45
  4. package/dist/components/billing/InvoicesPagination.js +2 -2
  5. package/dist/components/dashboard/block-editor/config-panel.d.ts.map +1 -1
  6. package/dist/components/dashboard/block-editor/config-panel.js +3 -2
  7. package/dist/components/dashboard/block-editor/entity-fields-sidebar.d.ts.map +1 -1
  8. package/dist/components/dashboard/block-editor/entity-fields-sidebar.js +3 -2
  9. package/dist/components/dashboard/navigation/DynamicNavigation.js +2 -2
  10. package/dist/components/settings/layouts/SettingsSidebar.d.ts.map +1 -1
  11. package/dist/components/settings/layouts/SettingsSidebar.js +0 -1
  12. package/dist/components/teams/CreateTeamButton.js +2 -2
  13. package/dist/components/teams/CreateTeamDialog.js +1 -1
  14. package/dist/components/teams/InviteMemberButton.js +3 -3
  15. package/dist/components/teams/InviteMemberDialog.js +1 -1
  16. package/dist/components/teams/TeamPendingInvitations.js +2 -2
  17. package/dist/components/users/tables/UsersTable.d.ts.map +1 -1
  18. package/dist/components/users/tables/UsersTable.js +3 -4
  19. package/dist/hooks/useAuthReadiness.d.ts +1 -0
  20. package/dist/hooks/useAuthReadiness.d.ts.map +1 -1
  21. package/dist/hooks/useAuthReadiness.js +3 -1
  22. package/dist/lib/actions/entity.actions.js +2 -2
  23. package/dist/lib/actions/team.actions.js +2 -2
  24. package/dist/lib/actions/user.actions.js +4 -4
  25. package/dist/lib/api/api-error.d.ts +2 -0
  26. package/dist/lib/api/api-error.d.ts.map +1 -1
  27. package/dist/lib/api/api-error.js +3 -1
  28. package/dist/lib/api/auth/dual-auth.d.ts +2 -1
  29. package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
  30. package/dist/lib/api/auth/dual-auth.js +25 -6
  31. package/dist/lib/api/auth/scope-policy.d.ts +6 -3
  32. package/dist/lib/api/auth/scope-policy.d.ts.map +1 -1
  33. package/dist/lib/api/auth.d.ts.map +1 -1
  34. package/dist/lib/api/auth.js +7 -0
  35. package/dist/lib/api/cors-response.d.ts +3 -2
  36. package/dist/lib/api/cors-response.d.ts.map +1 -1
  37. package/dist/lib/api/cors-response.js +8 -6
  38. package/dist/lib/api/entity/generic-handler.d.ts.map +1 -1
  39. package/dist/lib/api/entity/generic-handler.js +66 -15
  40. package/dist/lib/api/entity-handler.js +2 -2
  41. package/dist/lib/api/helpers.d.ts.map +1 -1
  42. package/dist/lib/api/helpers.js +9 -24
  43. package/dist/lib/api/permission-middleware.d.ts.map +1 -1
  44. package/dist/lib/api/permission-middleware.js +0 -2
  45. package/dist/lib/api/post-categories.d.ts +29 -0
  46. package/dist/lib/api/post-categories.d.ts.map +1 -0
  47. package/dist/lib/api/post-categories.js +33 -0
  48. package/dist/lib/api/rate-limit.d.ts +8 -0
  49. package/dist/lib/api/rate-limit.d.ts.map +1 -1
  50. package/dist/lib/api/rate-limit.js +34 -3
  51. package/dist/lib/api/request-origin.d.ts.map +1 -1
  52. package/dist/lib/api/request-origin.js +1 -1
  53. package/dist/lib/auth/auth-methods.d.ts +5 -2
  54. package/dist/lib/auth/auth-methods.d.ts.map +1 -1
  55. package/dist/lib/auth/auth-methods.js +4 -2
  56. package/dist/lib/auth/authorization-session.d.ts +14 -0
  57. package/dist/lib/auth/authorization-session.d.ts.map +1 -0
  58. package/dist/lib/auth/authorization-session.js +10 -0
  59. package/dist/lib/auth/request-session.d.ts +5 -0
  60. package/dist/lib/auth/request-session.d.ts.map +1 -1
  61. package/dist/lib/auth/request-session.js +3 -0
  62. package/dist/lib/auth/runtime-readiness.d.ts +7 -4
  63. package/dist/lib/auth/runtime-readiness.d.ts.map +1 -1
  64. package/dist/lib/auth/runtime-readiness.js +9 -6
  65. package/dist/lib/auth/sample-accounts.d.ts +18 -0
  66. package/dist/lib/auth/sample-accounts.d.ts.map +1 -0
  67. package/dist/lib/auth/sample-accounts.js +25 -0
  68. package/dist/lib/auth/suspension.d.ts +22 -0
  69. package/dist/lib/auth/suspension.d.ts.map +1 -0
  70. package/dist/lib/auth/suspension.js +43 -0
  71. package/dist/lib/auth.d.ts.map +1 -1
  72. package/dist/lib/auth.js +20 -17
  73. package/dist/lib/cache/public-entity-cache.d.ts +21 -0
  74. package/dist/lib/cache/public-entity-cache.d.ts.map +1 -0
  75. package/dist/lib/cache/public-entity-cache.js +25 -0
  76. package/dist/lib/config/app.config.d.ts.map +1 -1
  77. package/dist/lib/config/app.config.js +6 -7
  78. package/dist/lib/config/types.d.ts +8 -6
  79. package/dist/lib/config/types.d.ts.map +1 -1
  80. package/dist/lib/db-pool.d.ts +31 -0
  81. package/dist/lib/db-pool.d.ts.map +1 -0
  82. package/dist/lib/db-pool.js +75 -0
  83. package/dist/lib/db.d.ts +1 -1
  84. package/dist/lib/db.d.ts.map +1 -1
  85. package/dist/lib/db.js +11 -14
  86. package/dist/lib/entities/api-generator.js +5 -5
  87. package/dist/lib/entities/entity-hooks.d.ts.map +1 -1
  88. package/dist/lib/entities/entity-hooks.js +4 -0
  89. package/dist/lib/mcp/errors.d.ts +2 -0
  90. package/dist/lib/mcp/errors.d.ts.map +1 -1
  91. package/dist/lib/mcp/errors.js +3 -1
  92. package/dist/lib/mcp/index.d.ts +1 -1
  93. package/dist/lib/mcp/index.d.ts.map +1 -1
  94. package/dist/lib/mcp/index.js +2 -1
  95. package/dist/lib/oauth/token-refresh.d.ts.map +1 -1
  96. package/dist/lib/oauth/token-refresh.js +3 -6
  97. package/dist/lib/permissions/check.d.ts +9 -0
  98. package/dist/lib/permissions/check.d.ts.map +1 -1
  99. package/dist/lib/permissions/check.js +11 -1
  100. package/dist/lib/services/generic-entity.service.d.ts.map +1 -1
  101. package/dist/lib/services/generic-entity.service.js +41 -7
  102. package/dist/lib/services/transactional-meta.service.d.ts.map +1 -1
  103. package/dist/lib/services/transactional-meta.service.js +7 -8
  104. package/dist/lib/teams/actions.d.ts.map +1 -1
  105. package/dist/lib/teams/actions.js +0 -1
  106. package/dist/lib/validation.d.ts +5 -0
  107. package/dist/lib/validation.d.ts.map +1 -1
  108. package/dist/lib/validation.js +2 -0
  109. package/dist/messages/de/billing.json +12 -1
  110. package/dist/messages/de/common.json +2 -0
  111. package/dist/messages/de/docs.json +1 -0
  112. package/dist/messages/de/features.json +16 -4
  113. package/dist/messages/de/home.json +5 -0
  114. package/dist/messages/de/index.d.ts +68 -25
  115. package/dist/messages/de/index.d.ts.map +1 -1
  116. package/dist/messages/de/index.js +2 -0
  117. package/dist/messages/de/index.ts +2 -0
  118. package/dist/messages/de/settings.json +1 -0
  119. package/dist/messages/de/teams.json +3 -18
  120. package/dist/messages/en/billing.json +1 -1
  121. package/dist/messages/en/common.json +2 -0
  122. package/dist/messages/en/docs.json +1 -0
  123. package/dist/messages/en/features.json +16 -4
  124. package/dist/messages/en/home.json +5 -0
  125. package/dist/messages/en/index.d.ts +58 -25
  126. package/dist/messages/en/index.d.ts.map +1 -1
  127. package/dist/messages/en/index.js +2 -0
  128. package/dist/messages/en/index.ts +2 -0
  129. package/dist/messages/en/settings.json +1 -0
  130. package/dist/messages/en/teams.json +3 -18
  131. package/dist/messages/es/billing.json +1 -1
  132. package/dist/messages/es/common.json +2 -0
  133. package/dist/messages/es/docs.json +1 -0
  134. package/dist/messages/es/features.json +16 -4
  135. package/dist/messages/es/home.json +5 -0
  136. package/dist/messages/es/index.d.ts +41 -8
  137. package/dist/messages/es/index.d.ts.map +1 -1
  138. package/dist/messages/es/index.js +2 -0
  139. package/dist/messages/es/index.ts +2 -0
  140. package/dist/messages/es/settings.json +1 -0
  141. package/dist/messages/es/teams.json +3 -0
  142. package/dist/messages/fr/billing.json +12 -1
  143. package/dist/messages/fr/common.json +2 -0
  144. package/dist/messages/fr/docs.json +1 -0
  145. package/dist/messages/fr/features.json +16 -4
  146. package/dist/messages/fr/home.json +5 -0
  147. package/dist/messages/fr/index.d.ts +68 -25
  148. package/dist/messages/fr/index.d.ts.map +1 -1
  149. package/dist/messages/fr/index.js +2 -0
  150. package/dist/messages/fr/index.ts +2 -0
  151. package/dist/messages/fr/settings.json +1 -0
  152. package/dist/messages/fr/teams.json +3 -18
  153. package/dist/messages/it/billing.json +12 -1
  154. package/dist/messages/it/common.json +2 -0
  155. package/dist/messages/it/docs.json +1 -0
  156. package/dist/messages/it/features.json +16 -4
  157. package/dist/messages/it/home.json +5 -0
  158. package/dist/messages/it/index.d.ts +68 -25
  159. package/dist/messages/it/index.d.ts.map +1 -1
  160. package/dist/messages/it/index.js +2 -0
  161. package/dist/messages/it/index.ts +2 -0
  162. package/dist/messages/it/settings.json +1 -0
  163. package/dist/messages/it/teams.json +3 -18
  164. package/dist/messages/pt/billing.json +12 -1
  165. package/dist/messages/pt/common.json +2 -0
  166. package/dist/messages/pt/docs.json +1 -0
  167. package/dist/messages/pt/features.json +16 -4
  168. package/dist/messages/pt/home.json +5 -0
  169. package/dist/messages/pt/index.d.ts +68 -25
  170. package/dist/messages/pt/index.d.ts.map +1 -1
  171. package/dist/messages/pt/index.js +2 -0
  172. package/dist/messages/pt/index.ts +2 -0
  173. package/dist/messages/pt/settings.json +1 -0
  174. package/dist/messages/pt/teams.json +3 -18
  175. package/dist/migrations/029_sample_accounts_outside_development.sql +55 -0
  176. package/dist/migrations/030_taxonomies_team_writes.sql +29 -0
  177. package/dist/migrations/031_users_role_suspended.sql +41 -0
  178. package/dist/migrations/090_sample_data.sql +10 -0
  179. package/dist/routes/(auth)/signup/page.cc.d.ts +3 -1
  180. package/dist/routes/(auth)/signup/page.cc.d.ts.map +1 -1
  181. package/dist/routes/(auth)/signup/page.cc.js +2 -2
  182. package/dist/routes/(auth)/signup/page.d.ts +3 -1
  183. package/dist/routes/(auth)/signup/page.d.ts.map +1 -1
  184. package/dist/routes/(auth)/signup/page.js +4 -3
  185. package/dist/routes/_internal/area-access.js +2 -2
  186. package/dist/routes/_internal/entity-permission-layout.d.ts.map +1 -1
  187. package/dist/routes/_internal/entity-permission-layout.js +0 -7
  188. package/dist/routes/api/devtools/config/entities/route.js +2 -2
  189. package/dist/routes/api/devtools/config/theme/route.js +2 -2
  190. package/dist/routes/api/devtools/tests/[...path]/route.js +2 -2
  191. package/dist/routes/api/devtools/tests/route.js +2 -2
  192. package/dist/routes/api/mcp/route.d.ts.map +1 -1
  193. package/dist/routes/api/mcp/route.js +7 -1
  194. package/dist/routes/api/superadmin/subscriptions/route.js +2 -2
  195. package/dist/routes/api/superadmin/teams/[teamId]/route.js +2 -2
  196. package/dist/routes/api/superadmin/teams/route.js +2 -2
  197. package/dist/routes/api/superadmin/users/[userId]/route.d.ts +5 -3
  198. package/dist/routes/api/superadmin/users/[userId]/route.d.ts.map +1 -1
  199. package/dist/routes/api/superadmin/users/[userId]/route.js +46 -23
  200. package/dist/routes/api/superadmin/users/route.js +2 -2
  201. package/dist/routes/api/user/delete-account/route.d.ts.map +1 -1
  202. package/dist/routes/api/user/delete-account/route.js +2 -3
  203. package/dist/routes/api/user/plan-flags/route.d.ts.map +1 -1
  204. package/dist/routes/api/user/plan-flags/route.js +5 -5
  205. package/dist/routes/api/user/profile/route.js +3 -3
  206. package/dist/routes/api/v1/auth/signup-with-invite/route.d.ts.map +1 -1
  207. package/dist/routes/api/v1/auth/signup-with-invite/route.js +80 -44
  208. package/dist/routes/api/v1/media/upload/route.d.ts +2 -1
  209. package/dist/routes/api/v1/media/upload/route.d.ts.map +1 -1
  210. package/dist/routes/api/v1/media/upload/route.js +10 -2
  211. package/dist/routes/api/v1/post-categories/[id]/route.d.ts.map +1 -1
  212. package/dist/routes/api/v1/post-categories/[id]/route.js +39 -34
  213. package/dist/routes/api/v1/post-categories/route.d.ts.map +1 -1
  214. package/dist/routes/api/v1/post-categories/route.js +18 -14
  215. package/dist/routes/api/v1/team-invitations/route.d.ts.map +1 -1
  216. package/dist/routes/api/v1/team-invitations/route.js +12 -5
  217. package/dist/routes/api/v1/teams/[teamId]/members/route.d.ts.map +1 -1
  218. package/dist/routes/api/v1/teams/[teamId]/members/route.js +12 -10
  219. package/dist/routes/api/v1/teams/switch/route.d.ts.map +1 -1
  220. package/dist/routes/api/v1/teams/switch/route.js +4 -1
  221. package/dist/routes/dashboard/settings/password/page.d.ts.map +1 -1
  222. package/dist/routes/dashboard/settings/password/page.js +10 -12
  223. package/dist/routes/devtools/config/page.js +1 -1
  224. package/dist/routes/devtools/page.js +1 -1
  225. package/dist/routes/devtools/scheduled-actions/page.js +1 -1
  226. package/dist/routes/devtools/tests/[[...path]]/page.js +1 -1
  227. package/dist/routes/superadmin/users/page.js +2 -2
  228. package/dist/templates/ci-workflows/github/cypress-regression.yml +1 -1
  229. package/dist/templates/ci-workflows/github/cypress-smoke.yml +2 -1
  230. package/dist/templates/next.config.mjs +11 -31
  231. package/dist/templates/projects/blog/package.json +1 -1
  232. package/dist/templates/projects/crm/package.json +1 -1
  233. package/dist/templates/projects/productivity/package.json +1 -1
  234. package/dist/templates/projects/starter/README.md +2 -0
  235. package/dist/templates/projects/starter/config/app.config.ts +8 -0
  236. package/dist/templates/projects/starter/docs/public/01-overview/01-introduction.md +2 -0
  237. package/dist/templates/projects/starter/templates/dashboard/analytics/layout.tsx +14 -0
  238. package/dist/templates/proxy.ts +18 -8
  239. package/dist/templates/public/uploads/README.md +5 -94
  240. package/migrations/029_sample_accounts_outside_development.sql +55 -0
  241. package/migrations/030_taxonomies_team_writes.sql +29 -0
  242. package/migrations/031_users_role_suspended.sql +41 -0
  243. package/migrations/090_sample_data.sql +10 -0
  244. package/package.json +3 -3
  245. package/scripts/build/registry/__tests__/build-output.test.mjs +1 -1
  246. package/scripts/build/registry/__tests__/project-mode-root-first.test.mjs +6 -6
  247. package/scripts/build/registry/__tests__/write-places.test.mjs +1 -1
  248. package/scripts/build/registry/contracts/__tests__/contracts-wire.test.mjs +1 -1
  249. package/scripts/build/registry/contracts/__tests__/contracts.test.mjs +1 -1
  250. package/scripts/build/registry/host/README.md +2 -2
  251. package/scripts/build/registry/host/__tests__/facade-emitter.test.mjs +14 -13
  252. package/scripts/build/registry/host/__tests__/host-mode.test.mjs +1 -1
  253. package/scripts/build/registry/host/__tests__/host-project-composition.test.mjs +1 -1
  254. package/scripts/build/registry/host/__tests__/host-project.test.mjs +2 -2
  255. package/scripts/build/registry/host/__tests__/host-proxy-areas.test.mjs +99 -1
  256. package/scripts/build/registry/host/__tests__/host-robustness.test.mjs +1 -1
  257. package/scripts/build/registry/host/facade-emitter.mjs +1 -1
  258. package/scripts/build/registry/host/next-route-exports.json +2 -2
  259. package/scripts/build/registry/host/proxy-areas.mjs +92 -2
  260. package/scripts/db/run-migrations.mjs +30 -10
  261. package/scripts/db/sample-data.mjs +44 -0
  262. package/scripts/db/verify-theme-migrations.mjs +7 -1
  263. package/templates/ci-workflows/github/cypress-regression.yml +1 -1
  264. package/templates/ci-workflows/github/cypress-smoke.yml +2 -1
  265. package/templates/next.config.mjs +11 -31
  266. package/templates/projects/blog/package.json +1 -1
  267. package/templates/projects/crm/package.json +1 -1
  268. package/templates/projects/productivity/package.json +1 -1
  269. package/templates/projects/starter/README.md +2 -0
  270. package/templates/projects/starter/config/app.config.ts +8 -0
  271. package/templates/projects/starter/docs/public/01-overview/01-introduction.md +2 -0
  272. package/templates/projects/starter/templates/dashboard/analytics/layout.tsx +14 -0
  273. package/templates/proxy.ts +18 -8
  274. package/templates/public/uploads/README.md +5 -94
  275. package/tests/jest/__mocks__/@nextsparkjs/registries/api-presets-registry.ts +2 -0
  276. package/tests/jest/__mocks__/@nextsparkjs/registries/mcp-registry.ts +2 -0
  277. package/tests/jest/__mocks__/@nextsparkjs/registries/permissions-registry.ts +5 -4
  278. package/dist/templates/projects/starter/public/brand/.gitkeep +0 -0
  279. package/dist/templates/public/uploads/.gitignore +0 -6
  280. package/dist/templates/public/uploads/temp/.gitkeep +0 -1
  281. package/templates/projects/starter/public/brand/.gitkeep +0 -0
  282. package/templates/public/uploads/.gitignore +0 -6
  283. package/templates/public/uploads/temp/.gitkeep +0 -1
@@ -12,7 +12,7 @@ import { mkdtempSync, readFileSync, rmSync, symlinkSync } from 'node:fs'
12
12
  import { tmpdir } from 'node:os'
13
13
  import { join } from 'node:path'
14
14
 
15
- import { PROXY_AREA_WARNING, missingProxyAreas, proxyAreaNotice, proxyAreaNotices } from '../proxy-areas.mjs'
15
+ import { PROXY_AREA_WARNING, PROXY_SESSION_WARNING, PROXY_SESSION_CACHED_WARNING, missingProxyAreas, withoutComments, proxyAreaNotice, proxyAreaNotices, proxySessionNotice, proxySessionCachedNotice } from '../proxy-areas.mjs'
16
16
  import { predictHost } from '../prepare.mjs'
17
17
  import { PAGE, tempHost, write } from './host-helpers.mjs'
18
18
 
@@ -30,6 +30,20 @@ test('an area counts only when the proxy names its path; a look-alike prefix or
30
30
  assert.deepEqual(missingProxyAreas("const docs = '/superadmin-guide'; const tips = \"/devtools-tips\"").map(area => area.path), ['/superadmin', '/devtools'])
31
31
  })
32
32
 
33
+ test("a proxy that re-exports core's template has the check; a re-export of anything else does not", () => {
34
+ assert.deepEqual(missingProxyAreas("export { proxy } from '../../../packages/core/templates/proxy'\nexport const config = { matcher: [] }"), [])
35
+ assert.deepEqual(missingProxyAreas("export { proxy } from '@nextsparkjs/core/templates/proxy'"), [])
36
+ assert.deepEqual(missingProxyAreas("export { proxy } from './my-proxy'").map(area => area.path), ['/superadmin', '/devtools'])
37
+ assert.deepEqual(missingProxyAreas("export { other } from '@nextsparkjs/core/templates/proxy'").map(area => area.path), ['/superadmin', '/devtools'])
38
+ const both = ['/superadmin', '/devtools']
39
+ assert.deepEqual(missingProxyAreas("export { proxy as coreProxy } from '@nextsparkjs/core/templates/proxy'\nexport function proxy() {}").map(a => a.path), both)
40
+ assert.deepEqual(missingProxyAreas("export { other as proxy } from '@nextsparkjs/core/templates/proxy'").map(a => a.path), both)
41
+ assert.deepEqual(missingProxyAreas("export { proxy } from './vendor/core/templates/proxy'").map(a => a.path), both)
42
+ assert.deepEqual(missingProxyAreas("// export { proxy } from '@nextsparkjs/core/templates/proxy'\nexport function proxy() {}").map(a => a.path), both)
43
+ assert.deepEqual(missingProxyAreas("/*\nexport { proxy } from '@nextsparkjs/core/templates/proxy'\n*/\nexport function proxy() {}").map(a => a.path), both)
44
+ assert.deepEqual(missingProxyAreas("export { config, proxy } from '@nextsparkjs/core/templates/proxy.ts'"), [])
45
+ })
46
+
33
47
  test('the warning names the file, each missing area, its roles and where the check comes from', () => {
34
48
  const notice = proxyAreaNotice('src/proxy.ts', "const PROTECTED = ['/superadmin']")
35
49
  assert.equal(notice.code, PROXY_AREA_WARNING)
@@ -53,6 +67,63 @@ test('the proxy Next loads is the one checked; a project without one is not warn
53
67
  }
54
68
  })
55
69
 
70
+ // S80 (D1): the session check that fetched /api/auth/get-session from the request's origin, as every proxy.ts copied
71
+ // before 0.1.0-beta.196 does. Upgrading core never touches that copy, so prepare says what to change.
72
+ const HTTP_SESSION_CHECK = `function getSession(request: NextRequest) {
73
+ return betterFetch<Session>('/api/auth/get-session', {
74
+ baseURL: \`\${request.nextUrl.origin}\${request.nextUrl.basePath}\`,
75
+ headers: { cookie: request.headers.get('cookie') || '' },
76
+ })
77
+ }
78
+ const PROTECTED = ['/superadmin', '/devtools']
79
+ `
80
+
81
+ test('a proxy that asks its own /api/auth/get-session over HTTP is warned about, with the in-process call to use', () => {
82
+ const notice = proxySessionNotice('src/proxy.ts', HTTP_SESSION_CHECK)
83
+ assert.equal(notice.code, PROXY_SESSION_WARNING)
84
+ assert.match(notice.message, /^src\/proxy\.ts checks the session by fetching \/api\/auth\/get-session/)
85
+ assert.match(notice.message, /X-Forwarded-Proto: https/)
86
+ assert.match(notice.message, /auth\.api\.getSession\(\{ headers: new Headers\(\{ cookie: request\.headers\.get\('cookie'\) \|\| '' \}\), query: \{ disableRefresh: true, disableCookieCache: true \} \}\)/)
87
+ assert.equal(proxySessionNotice('src/proxy.ts', readFileSync(join(CORE_ROOT, 'templates/proxy.ts'), 'utf8')), null)
88
+ assert.equal(proxySessionNotice('src/proxy.ts', "// was: betterFetch('/api/auth/get-session')\nexport function proxy() {}"), null)
89
+ assert.equal(proxySessionNotice('src/proxy.ts', "export { proxy } from '@nextsparkjs/core/templates/proxy'"), null)
90
+ })
91
+
92
+ // The S80 review's measured edges: an end-of-line comment, and a '/*' / '*/' pair inside two string literals.
93
+ const COMMENT_EDGES = [
94
+ ['an end-of-line comment after code', "const x = 1 // was '/api/auth/get-session'\nexport function proxy() {}", false],
95
+ ['an end-of-line comment after code, no space', "const x = 1// was '/api/auth/get-session'", false],
96
+ ["a '/*' and a '*/' in two strings around the call", "const a = '/api/*'\nbetterFetch('/api/auth/get-session')\nconst b = 'x*/'", true],
97
+ ["a '//' inside a string before the call", "const u = 'https://example.com'; betterFetch('/api/auth/get-session')", true],
98
+ ['a block comment', "/* betterFetch('/api/auth/get-session') */ export function proxy() {}", false],
99
+ ['a quote escaped inside a string', "const q = 'it\\'s'; betterFetch('/api/auth/get-session')", true],
100
+ ]
101
+
102
+ test('comments are stripped wherever they start, and never inside a string literal', () => {
103
+ for (const [label, source, warns] of COMMENT_EDGES) {
104
+ assert.equal(proxySessionNotice('src/proxy.ts', source) !== null, warns, label)
105
+ }
106
+ assert.equal(withoutComments("a // b\nc"), 'a \nc')
107
+ assert.equal(withoutComments("'//' /* x */ \"/*\""), "'//' \"/*\"")
108
+ // The repository template, old and new, reads the same with comments gone: every regex literal in it survives.
109
+ const template = readFileSync(join(CORE_ROOT, 'templates/proxy.ts'), 'utf8')
110
+ assert.match(withoutComments(template), /\.replace\(\/\\\\\/g, '\/'\)\.replace\(\/\\\/\\\/\+\/g, '\/'\)/)
111
+ })
112
+
113
+ test("the proxy Next loads gets the session warning next to the areas one; core's template gets neither", () => {
114
+ const root = mkdtempSync(join(tmpdir(), 'nextspark-proxy-session-'))
115
+ try {
116
+ write(root, 'src/proxy.ts', HTTP_SESSION_CHECK)
117
+ assert.deepEqual(proxyAreaNotices(root).map(notice => notice.code), [PROXY_SESSION_WARNING])
118
+ write(root, 'src/proxy.ts', `${HTTP_SESSION_CHECK.replace("['/superadmin', '/devtools']", '[]')}`)
119
+ assert.deepEqual(proxyAreaNotices(root).map(notice => notice.code), [PROXY_AREA_WARNING, PROXY_SESSION_WARNING])
120
+ write(root, 'src/proxy.ts', readFileSync(join(CORE_ROOT, 'templates/proxy.ts'), 'utf8'))
121
+ assert.deepEqual(proxyAreaNotices(root), [])
122
+ } finally {
123
+ rmSync(root, { recursive: true, force: true })
124
+ }
125
+ })
126
+
56
127
  test("prepare's prediction carries the warning among its notices", async () => {
57
128
  const host = tempHost({ 'templates/about/page.tsx': PAGE('About') })
58
129
  const project = mkdtempSync(join(tmpdir(), 'nextspark-proxy-areas-project-'))
@@ -67,3 +138,30 @@ test("prepare's prediction carries the warning among its notices", async () => {
67
138
  rmSync(project, { recursive: true, force: true })
68
139
  }
69
140
  })
141
+
142
+ // The in-process session read of the 0.1.0-beta.196 template, without disableCookieCache.
143
+ const BETA_196_SESSION_READ = `async function getSession(request: NextRequest): Promise<Session | null> {
144
+ const session = await auth.api.getSession({
145
+ headers: new Headers({ cookie: request.headers.get('cookie') || '' }),
146
+ query: { disableRefresh: true },
147
+ })
148
+ return session as Session | null
149
+ }
150
+ `
151
+
152
+ test('a proxy that reads the session in process from the cookie cache is warned about, with the option to add', () => {
153
+ const notice = proxySessionCachedNotice('src/proxy.ts', BETA_196_SESSION_READ)
154
+ assert.equal(notice.code, PROXY_SESSION_CACHED_WARNING)
155
+ assert.match(notice.message, /^src\/proxy\.ts reads the session with auth\.api\.getSession from Better Auth's cookie cache/)
156
+ assert.match(notice.message, /query: \{ disableRefresh: true, disableCookieCache: true \}/)
157
+ assert.equal(proxySessionCachedNotice('src/proxy.ts', readFileSync(join(CORE_ROOT, 'templates/proxy.ts'), 'utf8')), null)
158
+ assert.match(notice.message, /role !== 'suspended'/)
159
+ // the cache option alone is not enough: the proxy must also refuse a suspended account's session
160
+ const cacheOnly = BETA_196_SESSION_READ.replace('disableRefresh: true', 'disableRefresh: true, disableCookieCache: true')
161
+ assert.notEqual(proxySessionCachedNotice('src/proxy.ts', cacheOnly), null)
162
+ assert.equal(proxySessionCachedNotice('src/proxy.ts', cacheOnly.replace('return session as Session | null', "return session && (session.user as { role?: unknown }).role !== 'suspended' ? (session as Session) : null")), null)
163
+ // a comment that names the option does not count
164
+ assert.notEqual(proxySessionCachedNotice('src/proxy.ts', `// disableCookieCache\n${BETA_196_SESSION_READ}`), null)
165
+ // no in-process read (the HTTP check has its own warning), no notice
166
+ assert.equal(proxySessionCachedNotice('src/proxy.ts', HTTP_SESSION_CHECK), null)
167
+ })
@@ -314,7 +314,7 @@ test('an npm-installed core ships only dist/: the manifest, variants and route m
314
314
  test('the project config is read again for every generation: enabling a plugin in nextspark.config.ts adds its routes', async () => {
315
315
  const root = mkdtempSync(join(tmpdir(), 'nextspark-config-reload-'))
316
316
  try {
317
- write(root, 'package.json', JSON.stringify({ name: 'reload', dependencies: { next: '16.3.6' } }))
317
+ write(root, 'package.json', JSON.stringify({ name: 'reload', dependencies: { next: '16.3.8' } }))
318
318
  write(root, 'nextspark.config.ts', 'export default { plugins: [] }\n')
319
319
  write(root, 'plugins/search/plugin.config.ts', 'export const searchPluginConfig = {}\n')
320
320
  write(root, 'plugins/search/templates/search/page.tsx', PAGE('Search'))
@@ -284,7 +284,7 @@ export function resolveNextPackage(projectRoot = process.cwd()) {
284
284
  /**
285
285
  * Whether `version` is a Next.js the table applies to: the table's `next` is the verified
286
286
  * baseline, and core pins `next` to `~<baseline>`, so any stable patch of the same minor at or
287
- * above the baseline is accepted (16.3.6, 16.3.9, 16.3.6+build.1), while other minors/majors, lower
287
+ * above the baseline is accepted (16.3.8, 16.3.9, 16.3.8+build.1), while other minors/majors, lower
288
288
  * patches and prereleases (16.3.7-canary.0) are refused.
289
289
  */
290
290
  export function isSupportedNextVersion(version, baseline = ROUTE_EXPORT_TABLE.next) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$comment": "Route file kinds x exports x emitted facade form, for the generated host (#203). Source of truth for host/facade-emitter.mjs; README.md renders it. Verified against the installed Next.js source listed in verifiedAgainst and by the probe builds recorded in README.md. Re-verify and bump `next` when upgrading Next.js: host/__tests__/facade-emitter.test.mjs fails when the installed version differs.",
3
- "next": "16.3.6",
4
- "supportedNext": "~16.3.6 (same major.minor, stable patch >= the verified baseline in \"next\"; enforced by isSupportedNextVersion)",
3
+ "next": "16.3.8",
4
+ "supportedNext": "~16.3.8 (same major.minor, stable patch >= the verified baseline in \"next\"; enforced by isSupportedNextVersion)",
5
5
  "verifiedAgainst": [
6
6
  "next/dist/build/segment-config/app/app-segment-config.js (AppSegmentConfigSchemaKeys, parseAppSegmentConfig)",
7
7
  "next/dist/build/analysis/get-page-static-info.js (checkExports, getAppPageStaticInfo)",
@@ -7,6 +7,10 @@
7
7
  * before the template gained the check, or written by hand, may not have it: `nextspark prepare` (and `migrate`, in
8
8
  * the CLI) warn, naming each area the proxy never mentions.
9
9
  *
10
+ * Same for the session check (S80): a proxy copied before 0.1.0-beta.196 fetches its own /api/auth/get-session from
11
+ * the request's origin, which behind a proxy that terminates TLS is https on a plain-HTTP port, so every signed-in
12
+ * user is sent to /login. That copy is never updated by an upgrade, so prepare and migrate say what to change.
13
+ *
10
14
  * @module core/scripts/build/registry/host/proxy-areas
11
15
  */
12
16
 
@@ -24,8 +28,45 @@ export const PROTECTED_AREAS = Object.freeze([
24
28
  /** Where Next loads a project's proxy from, next to the generated src/app. */
25
29
  export const PROXY_FILES = Object.freeze(['src/proxy.ts', 'src/proxy.js', 'src/middleware.ts', 'src/middleware.js'])
26
30
 
31
+ /**
32
+ * A proxy that re-exports core's template under its own name (apps/dev does) has the check without naming the areas
33
+ * itself. Exact name (`proxy as x` is not the proxy Next loads), a concrete specifier (the package, or the repository's
34
+ * packages/core), and only code: a commented-out line does not count.
35
+ */
36
+ const REEXPORTS_CORE_PROXY =
37
+ /^\s*export\s*\{\s*(?:[\w$]+\s*,\s*)*proxy\s*(?:,[^}]*)?\}\s*from\s*['"](?:@nextsparkjs\/core|(?:\.\.\/)+packages\/core)\/templates\/proxy(?:\.ts)?['"]/m
38
+ /**
39
+ * The source without its comments (`//` to the end of the line, wherever it starts, and block comments). String
40
+ * literals are kept whole, so a `/*` or `//` inside one is text. Not parsed: regular-expression literals (a quote in
41
+ * one can hide the code after it) and template-literal `${}` nesting; nor does the session detector follow a URL
42
+ * assembled from parts. No template that copied the HTTP session check needs either.
43
+ */
44
+ export function withoutComments(source) {
45
+ let out = ''
46
+ let quote = null
47
+ for (let i = 0; i < source.length; i++) {
48
+ const c = source[i]
49
+ if (quote) {
50
+ out += c
51
+ if (c === '\\') out += source[++i] ?? ''
52
+ else if (c === quote) quote = null
53
+ } else if (c === '/' && source[i + 1] === '/') {
54
+ while (i + 1 < source.length && source[i + 1] !== '\n') i++
55
+ } else if (c === '/' && source[i + 1] === '*') {
56
+ const end = source.indexOf('*/', i + 2)
57
+ i = end < 0 ? source.length : end + 1
58
+ out += ' '
59
+ } else {
60
+ if (c === "'" || c === '"' || c === '`') quote = c
61
+ out += c
62
+ }
63
+ }
64
+ return out
65
+ }
66
+
27
67
  /** The protected areas a proxy's source never names as a path (`'/superadmin'`, `"/devtools"`, `` `/devtools` ``). */
28
68
  export function missingProxyAreas(source) {
69
+ if (REEXPORTS_CORE_PROXY.test(withoutComments(source))) return []
29
70
  return PROTECTED_AREAS.filter(area => !new RegExp(`['"\`]${area.path}(?![\\w-])`).test(source))
30
71
  }
31
72
 
@@ -46,11 +87,60 @@ export function proxyAreaNotice(file, source) {
46
87
  }
47
88
  }
48
89
 
90
+ export const PROXY_SESSION_WARNING = 'NS_PROXY_SESSION_OVER_HTTP'
91
+
92
+ /** What to write instead of the HTTP session check; migrate in the CLI says the same. */
93
+ export const PROXY_SESSION_FIX =
94
+ "import { auth } from '@nextsparkjs/core/lib/auth' and replace the betterFetch('/api/auth/get-session', ...) call with " +
95
+ "auth.api.getSession({ headers: new Headers({ cookie: request.headers.get('cookie') || '' }), query: { disableRefresh: true, disableCookieCache: true } }), " +
96
+ 'which returns the session itself (or null), not { data }'
97
+
98
+ /** The warning for a proxy whose code (not a comment) asks /api/auth/get-session over HTTP, or null. */
99
+ export function proxySessionNotice(file, source) {
100
+ if (!withoutComments(source).includes('/api/auth/get-session')) return null
101
+ return {
102
+ code: PROXY_SESSION_WARNING,
103
+ target: file,
104
+ message:
105
+ `${file} checks the session by fetching /api/auth/get-session from the request's own origin. Behind a proxy that ` +
106
+ `terminates TLS (X-Forwarded-Proto: https) that origin is https on a port that speaks plain HTTP: the fetch fails ` +
107
+ `and every signed-in user is sent to /login. Read the session in process, as ` +
108
+ `node_modules/@nextsparkjs/core/templates/proxy.ts does: ${PROXY_SESSION_FIX}`,
109
+ }
110
+ }
111
+
112
+ export const PROXY_SESSION_CACHED_WARNING = 'NS_PROXY_SESSION_CACHED'
113
+
114
+ /** What to add to an in-process session read (the 0.1.0-beta.196 template); migrate in the CLI says the same. */
115
+ export const PROXY_SESSION_CACHED_FIX =
116
+ "add disableCookieCache: true to the query of its auth.api.getSession call: query: { disableRefresh: true, disableCookieCache: true }, " +
117
+ "and refuse a suspended account's session at the end of getSession: return session && (session.user as { role?: unknown }).role !== 'suspended' ? (session as Session) : null"
118
+
119
+ /**
120
+ * The warning for a proxy whose code (not a comment) reads the session in process (auth.api.getSession, as the
121
+ * 0.1.0-beta.196 template) without disableCookieCache, or without refusing a suspended account, or null: Better Auth
122
+ * then answers from the cached session_data cookie, so a signed-out cookie pair or an old role passes the proxy for up
123
+ * to 5 minutes, and a suspended account's session passes it.
124
+ */
125
+ export function proxySessionCachedNotice(file, source) {
126
+ const code = withoutComments(source)
127
+ if (!code.includes('auth.api.getSession(') || (code.includes('disableCookieCache') && code.includes("'suspended'"))) return null
128
+ return {
129
+ code: PROXY_SESSION_CACHED_WARNING,
130
+ target: file,
131
+ message:
132
+ `${file} reads the session with auth.api.getSession from Better Auth's cookie cache: after a sign-out, the copied cookie ` +
133
+ `pair, or a role changed since sign-in, still passes the proxy until the cached cookie expires (5 minutes by default), ` +
134
+ `and a suspended account's session passes it. ` +
135
+ `As node_modules/@nextsparkjs/core/templates/proxy.ts does, ${PROXY_SESSION_CACHED_FIX}`,
136
+ }
137
+ }
138
+
49
139
  /** The warnings for the proxy Next would load in `projectRoot` (the first of PROXY_FILES that exists). */
50
140
  export function proxyAreaNotices(projectRoot) {
51
141
  if (!projectRoot) return []
52
142
  const file = PROXY_FILES.find(candidate => existsSync(join(projectRoot, candidate)))
53
143
  if (!file) return []
54
- const notice = proxyAreaNotice(file, readFileSync(join(projectRoot, file), 'utf8'))
55
- return notice ? [notice] : []
144
+ const source = readFileSync(join(projectRoot, file), 'utf8')
145
+ return [proxyAreaNotice(file, source), proxySessionNotice(file, source), proxySessionCachedNotice(file, source)].filter(Boolean)
56
146
  }
@@ -2,6 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { migrationTimeLimit, migrationClient, ignoredParametersNotice, runAndRecordMigration, migrationFailure } from './migration-time-limit.mjs';
4
4
  import { getConfig } from '../build/registry/config.mjs';
5
+ import { isSampleDataMigration, sampleDataPolicy } from './sample-data.mjs';
5
6
 
6
7
  const projectConfig = getConfig();
7
8
  const projectRoot = projectConfig.projectRoot;
@@ -21,6 +22,8 @@ let DATABASE_URL = process.env.DATABASE_URL ?? null;
21
22
  // Falls back to DATABASE_URL when unset (pre-cutover: same owner connection).
22
23
  let MIGRATE_DATABASE_URL = process.env.MIGRATE_DATABASE_URL ?? null;
23
24
  let MIGRATION_TIMEOUT_SECONDS = process.env.MIGRATION_TIMEOUT_SECONDS;
25
+ // What the .env says about NODE_ENV and the sample-data switch (see sample-data.mjs)
26
+ const fileEnv = {};
24
27
 
25
28
  if (readEnvFile && fs.existsSync(envPath)) {
26
29
  const envContent = fs.readFileSync(envPath, 'utf8');
@@ -40,6 +43,11 @@ if (readEnvFile && fs.existsSync(envPath)) {
40
43
  if (key?.trim() === 'MIGRATION_TIMEOUT_SECONDS' && valueParts.length > 0) {
41
44
  MIGRATION_TIMEOUT_SECONDS = value;
42
45
  }
46
+ // `export NODE_ENV=...` counts too; sample-data.mjs reads the value itself (case, trailing comment)
47
+ const name = key?.trim().replace(/^export\s+/, '');
48
+ if (['NODE_ENV', 'NEXTSPARK_SEED_SAMPLE_DATA'].includes(name) && valueParts.length > 0) {
49
+ fileEnv[name] = value;
50
+ }
43
51
  }
44
52
  });
45
53
  }
@@ -65,12 +73,20 @@ try {
65
73
  const ignoredParameters = ignoredParametersNotice(MIGRATION_URL, TIME_LIMIT);
66
74
  if (ignoredParameters) console.log(`⚠️ ${ignoredParameters}\n`);
67
75
 
76
+ const SAMPLE_DATA = sampleDataPolicy({ argv: process.argv, env: process.env, fileEnv });
77
+
68
78
  async function runMigrations() {
69
79
  const client = migrationClient(MIGRATION_URL, TIME_LIMIT);
70
80
 
71
81
  try {
72
82
  await client.connect();
73
83
  console.log("✅ Connected to database\n");
84
+
85
+ // 029_sample_accounts_outside_development.sql reads it: a run that applies sample data keeps the sample accounts
86
+ await client.query("SELECT set_config('nextspark.seed_sample_data', $1, false)", [SAMPLE_DATA.apply ? 'on' : 'off']);
87
+ client.on('notice', notice => {
88
+ if (notice.message?.startsWith('Sample accounts:')) console.log(`🔐 ${notice.message}`);
89
+ });
74
90
 
75
91
  // Create migrations tracking table
76
92
  await client.query(`
@@ -101,9 +117,14 @@ async function runMigrations() {
101
117
  continue;
102
118
  }
103
119
 
120
+ const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf8');
121
+ if (!SAMPLE_DATA.apply && isSampleDataMigration(file, sql)) {
122
+ console.log(`⏭️ Skipping ${file} (sample data)`);
123
+ continue;
124
+ }
125
+
104
126
  // Read and execute migration
105
127
  console.log(`🔄 Running ${file}...`);
106
- const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf8');
107
128
 
108
129
  try {
109
130
  // Runs the file and records it in one transaction (see runAndRecordMigration)
@@ -141,11 +162,6 @@ async function getActivePlugins() {
141
162
  return projectConfig.plugins;
142
163
  }
143
164
 
144
- // Helper: Check if a filename is a sample_data migration
145
- function isSampleDataMigration(filename) {
146
- return filename.toLowerCase().includes('sample_data') || filename.toLowerCase().includes('sample-data');
147
- }
148
-
149
165
  // Helper: Collect all migration files from a directory (theme/plugin level)
150
166
  function collectContentMigrations(migrationsPath, sourceType, sourceName) {
151
167
  if (!fs.existsSync(migrationsPath)) return [];
@@ -160,7 +176,7 @@ function collectContentMigrations(migrationsPath, sourceType, sourceName) {
160
176
  sourceName,
161
177
  filename: file,
162
178
  fullPath: path.join(migrationsPath, file),
163
- isSampleData: isSampleDataMigration(file)
179
+ isSampleData: isSampleDataMigration(file, fs.readFileSync(path.join(migrationsPath, file), 'utf8'))
164
180
  }));
165
181
  }
166
182
 
@@ -193,7 +209,7 @@ function collectEntityMigrations(baseDir, sourceType, sourceName, depth = 0) {
193
209
  entityName,
194
210
  filename: file,
195
211
  fullPath: path.join(migrationsPath, file),
196
- isSampleData: isSampleDataMigration(file),
212
+ isSampleData: isSampleDataMigration(file, fs.readFileSync(path.join(migrationsPath, file), 'utf8')),
197
213
  depth
198
214
  });
199
215
  }
@@ -415,15 +431,18 @@ async function runEntityMigrations() {
415
431
  const sortByFilename = (a, b) => a.filename.localeCompare(b.filename);
416
432
 
417
433
  const contentSchema = allContentMigrations.filter(m => !m.isSampleData).sort(sortByFilename);
418
- const contentSampleData = allContentMigrations.filter(m => m.isSampleData).sort(sortByFilename);
434
+ const contentSampleData = allContentMigrations.filter(m => m.isSampleData && SAMPLE_DATA.apply).sort(sortByFilename);
419
435
  const entitySchema = allEntityMigrations.filter(m => !m.isSampleData).sort(sortByFilename);
420
- const entitySampleData = allEntityMigrations.filter(m => m.isSampleData).sort(sortByFilename);
436
+ const entitySampleData = allEntityMigrations.filter(m => m.isSampleData && SAMPLE_DATA.apply).sort(sortByFilename);
421
437
 
422
438
  console.log(`📊 Migration breakdown:`);
423
439
  console.log(` - Content schema: ${contentSchema.length}`);
424
440
  console.log(` - Content sample_data: ${contentSampleData.length}`);
425
441
  console.log(` - Entity schema: ${entitySchema.length}`);
426
442
  console.log(` - Entity sample_data: ${entitySampleData.length}`);
443
+ for (const m of [...allContentMigrations, ...allEntityMigrations].filter(m => m.isSampleData && !SAMPLE_DATA.apply)) {
444
+ console.log(`⏭️ Skipping ${m.filename} (sample data, ${m.sourceName})`);
445
+ }
427
446
  console.log('');
428
447
 
429
448
  let totalContentMigrations = 0;
@@ -550,6 +569,7 @@ async function runEntityMigrations() {
550
569
  // Main migration runner
551
570
  async function runAllMigrations() {
552
571
  console.log("🚀 Starting migration process...\n");
572
+ console.log(`🌱 ${SAMPLE_DATA.notice}\n`);
553
573
 
554
574
  // First run core migrations
555
575
  console.log("📋 PHASE 1: Core migrations");
@@ -0,0 +1,44 @@
1
+ // Which migrations are sample data, and whether a db:migrate run applies them.
2
+ //
3
+ // Sample data is applied only when the run asks for it -- `--sample-data`
4
+ // (what `nextspark db:seed` passes) or NEXTSPARK_SEED_SAMPLE_DATA=1, in the
5
+ // environment or the project .env -- and never when NODE_ENV is production in
6
+ // either: its accounts have a documented development password. A sample-data
7
+ // file that is not applied is not recorded either, so a later run that asks
8
+ // for it still applies it.
9
+
10
+ export const SAMPLE_DATA_FLAG = '--sample-data';
11
+ export const SAMPLE_DATA_VARIABLE = 'NEXTSPARK_SEED_SAMPLE_DATA';
12
+ /** A line that marks a migration as sample data when its name does not say so. */
13
+ export const SAMPLE_DATA_MARKER = '-- nextspark:sample-data';
14
+
15
+ // A value as a .env or a shell may write it: any case, quotes, a trailing comment.
16
+ const isProduction = value => /^\s*["']?production\b/i.test(String(value ?? ''));
17
+ const isOn = value => /^\s*["']?(1|true)\b/i.test(String(value ?? ''));
18
+
19
+ // Copies made before the name rule and the marker existed (the default theme's
20
+ // 090_demo_users_teams.sql and 091_greek_teams_billing.sql, up to 0.1.0-beta.191)
21
+ // are recognised by what they insert: a credential with a sample password. The
22
+ // salts are those of migrations/029_*.sql, which has them but inserts nothing.
23
+ const SAMPLE_PASSWORD_SALTS = ['22de14d5472248ed0bece911df908b2a:', '3db9e98e2b4d3caca97fdf2783791cbc:'];
24
+ const insertsSamplePassword = sql => /insert\s+into\s+(?:"?public"?\s*\.\s*)?"?account\b/i.test(sql) && SAMPLE_PASSWORD_SALTS.some(salt => sql.includes(salt));
25
+
26
+ export function isSampleDataMigration(filename, sql = '') {
27
+ const name = filename.toLowerCase();
28
+ return name.includes('sample_data') || name.includes('sample-data') || sql.split('\n').some(line => line.trim() === SAMPLE_DATA_MARKER) || insertsSamplePassword(sql);
29
+ }
30
+
31
+ /**
32
+ * `env` is the process environment, `fileEnv` what the project .env sets.
33
+ * Returns whether sample data is applied and the line the runner prints about it.
34
+ */
35
+ export function sampleDataPolicy({ argv = [], env = {}, fileEnv = {} }) {
36
+ const requested = argv.includes(SAMPLE_DATA_FLAG) || [env[SAMPLE_DATA_VARIABLE], fileEnv[SAMPLE_DATA_VARIABLE]].some(isOn);
37
+ if ([env.NODE_ENV, fileEnv.NODE_ENV].some(isProduction)) {
38
+ return { apply: false, notice: `Sample data is not applied: NODE_ENV is production${requested ? ', which the sample-data switch does not override' : ''}.` };
39
+ }
40
+ if (requested) {
41
+ return { apply: true, notice: 'Sample data is applied (development). Its accounts share a documented password: use this only on a local database, and never use a database seeded this way in production (its sample accounts are kept, and later migrations do not disable them).' };
42
+ }
43
+ return { apply: false, notice: `Sample data is not applied. For a local development database, run \`pnpm db:seed\` (or pass ${SAMPLE_DATA_FLAG}, or set ${SAMPLE_DATA_VARIABLE}=1).` };
44
+ }
@@ -28,6 +28,9 @@
28
28
  // `pnpm db:migrate` uses. Templates are first extracted into a clean temporary
29
29
  // root-first project; apps/dev already is one. No .env is read or written.
30
30
  //
31
+ // The run applies the theme's sample data (the database is empty and
32
+ // throwaway), so NODE_ENV is development for it whatever the shell says.
33
+ //
31
34
  // MIGRATE_DATABASE_URL is removed from the child's environment because
32
35
  // run-migrations.mjs connects to it in preference to DATABASE_URL, which
33
36
  // would migrate a database this script never checked.
@@ -46,6 +49,7 @@ import { GLOBAL_OBJECTS, inspectCluster } from './cluster-changes.mjs';
46
49
  import { inspectTarget, inspectMaintenanceDatabase } from './inspect-server.mjs';
47
50
  import { findTheme } from './theme-location.mjs';
48
51
  import { TIME_LIMIT_VARIABLE, migrationTimeLimit } from './migration-time-limit.mjs';
52
+ import { SAMPLE_DATA_VARIABLE } from './sample-data.mjs';
49
53
 
50
54
  const __filename = fileURLToPath(import.meta.url);
51
55
  const __dirname = path.dirname(__filename);
@@ -153,7 +157,7 @@ async function main() {
153
157
  extracted = fs.mkdtempSync(path.join(os.tmpdir(), `nextspark-template-${theme}-`));
154
158
  fs.cpSync(located.sourceDir, extracted, { recursive: true });
155
159
  fs.writeFileSync(path.join(extracted, 'nextspark.config.ts'), `export default { plugins: [], template: { name: ${JSON.stringify(theme)}, version: 'test' } }\n`);
156
- fs.writeFileSync(path.join(extracted, 'package.json'), JSON.stringify({ name: `nextspark-template-${theme}`, private: true, dependencies: { next: '16.3.6' } }, null, 2));
160
+ fs.writeFileSync(path.join(extracted, 'package.json'), JSON.stringify({ name: `nextspark-template-${theme}`, private: true, dependencies: { next: '16.3.8' } }, null, 2));
157
161
  runDir = extracted;
158
162
  }
159
163
 
@@ -164,6 +168,8 @@ async function main() {
164
168
  ...inheritedEnv,
165
169
  DATABASE_URL: databaseUrl,
166
170
  [TIME_LIMIT_VARIABLE]: timeLimitSetting,
171
+ [SAMPLE_DATA_VARIABLE]: '1',
172
+ NODE_ENV: 'development',
167
173
  },
168
174
  });
169
175
  if (extracted) fs.rmSync(extracted, { recursive: true, force: true });
@@ -66,7 +66,7 @@ jobs:
66
66
  - name: Seed test data
67
67
  env:
68
68
  DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
69
- run: pnpm db:seed || echo "No seed script found, skipping..."
69
+ run: pnpm db:seed
70
70
 
71
71
  - name: Build application
72
72
  run: pnpm build
@@ -59,7 +59,8 @@ jobs:
59
59
  - name: Setup database
60
60
  env:
61
61
  DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
62
- run: pnpm db:migrate
62
+ # a throwaway test database: migrations plus the sample users the tests sign in as
63
+ run: pnpm db:seed
63
64
 
64
65
  - name: Build application
65
66
  run: pnpm build
@@ -109,42 +109,21 @@ const nextConfig = {
109
109
  formats: ['image/avif', 'image/webp'],
110
110
  deviceSizes: [640, 750, 828, 1080, 1200, 1920],
111
111
  imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
112
+ // Hosts the server may fetch and resize through /_next/image.
113
+ // Keep this list to the exact hosts you use.
112
114
  remotePatterns: [
115
+ // Google profile pictures shown for accounts that sign in with Google.
113
116
  {
114
117
  protocol: 'https',
115
118
  hostname: 'lh3.googleusercontent.com',
116
119
  pathname: '/**',
117
120
  },
118
- {
119
- protocol: 'https',
120
- hostname: '*.public.blob.vercel-storage.com',
121
- pathname: '/**',
122
- },
123
- {
124
- protocol: 'https',
125
- hostname: 'images.unsplash.com',
126
- pathname: '/**',
127
- },
128
- {
129
- protocol: 'https',
130
- hostname: 'upload.wikimedia.org',
131
- pathname: '/**',
132
- },
133
- {
134
- protocol: 'https',
135
- hostname: 'i.pravatar.cc',
136
- pathname: '/**',
137
- },
138
- {
139
- protocol: 'https',
140
- hostname: '*.supabase.co',
141
- pathname: '/**',
142
- },
143
- {
144
- protocol: 'https',
145
- hostname: '*.cloudinary.com',
146
- pathname: '/**',
147
- },
121
+ // Examples: add the exact host of your own store, e.g. your Vercel Blob store
122
+ // (`<store-id>.public.blob.vercel-storage.com`) once uploads use it.
123
+ // { protocol: 'https', hostname: '<store-id>.public.blob.vercel-storage.com', pathname: '/**' },
124
+ // { protocol: 'https', hostname: '<project-ref>.supabase.co', pathname: '/**' },
125
+ // { protocol: 'https', hostname: 'res.cloudinary.com', pathname: '/<cloud-name>/**' },
126
+ // { protocol: 'https', hostname: 'images.unsplash.com', pathname: '/**' },
148
127
  ],
149
128
  },
150
129
  // Next.js sets TURBOPACK before it loads this file whenever Turbopack builds
@@ -158,7 +137,8 @@ const nextConfig = {
158
137
  async headers() {
159
138
  const isProduction = process.env.NODE_ENV === 'production';
160
139
 
161
- // Allowed image domains (must match remotePatterns above)
140
+ // Image hosts the browser may load directly (CSP img-src). The server only fetches the hosts in
141
+ // images.remotePatterns above; add a host there too when next/image should serve it.
162
142
  // NOTE: Wildcard patterns (*.public.blob.vercel-storage.com, *.supabase.co, *.cloudinary.com)
163
143
  // allow images from any account on these services for development flexibility.
164
144
  // For production with stricter security, consider restricting to specific account subdomains.
@@ -14,7 +14,7 @@
14
14
  "@nextsparkjs/core": ">=0.1.0-0",
15
15
  "@nextsparkjs/testing": ">=0.1.0-0",
16
16
  "lucide-react": "^0.539.0",
17
- "next": "~16.3.6",
17
+ "next": "~16.3.8",
18
18
  "next-intl": "^4.0.0",
19
19
  "react": "^19.2.0",
20
20
  "react-dom": "^19.2.0",
@@ -13,7 +13,7 @@
13
13
  "@nextsparkjs/core": ">=0.1.0-0",
14
14
  "@nextsparkjs/testing": ">=0.1.0-0",
15
15
  "lucide-react": "^0.539.0",
16
- "next": "~16.3.6",
16
+ "next": "~16.3.8",
17
17
  "next-intl": "^4.0.0",
18
18
  "react": "^19.2.0",
19
19
  "react-dom": "^19.2.0",
@@ -17,7 +17,7 @@
17
17
  "@nextsparkjs/core": ">=0.1.0-0",
18
18
  "@nextsparkjs/testing": ">=0.1.0-0",
19
19
  "lucide-react": "^0.539.0",
20
- "next": "~16.3.6",
20
+ "next": "~16.3.8",
21
21
  "react": "^19.2.0",
22
22
  "react-dom": "^19.2.0",
23
23
  "zod": "^4.0.0"
@@ -21,6 +21,8 @@ cp .env.example .env.local
21
21
 
22
22
  # Run migrations
23
23
  pnpm db:migrate
24
+ # Local development only: also load the sample users (DevKeyring, Cypress)
25
+ pnpm db:seed
24
26
 
25
27
  # Start development
26
28
  pnpm dev
@@ -126,6 +126,14 @@ export const APP_CONFIG_OVERRIDES = {
126
126
  // methods: ['email-password', 'google'],
127
127
  // methods: ['email-otp', 'email-password', 'google'],
128
128
 
129
+ /**
130
+ * Password endpoints (sign-in/email, sign-up/email, reset). Left undeclared
131
+ * they follow `methods` in production (off under the passwordless preset) and
132
+ * stay on in development (dev keyring, `pnpm db:seed` users, Cypress).
133
+ * Uncomment to keep password logins in production (API clients, test users):
134
+ */
135
+ // emailAndPassword: { enabled: true },
136
+
129
137
  /**
130
138
  * Session duration / renewal in seconds (defaults: 7 days, renewed daily).
131
139
  * Long-lived sessions for an installed PWA, for example:
@@ -17,6 +17,8 @@ Welcome to the Starter theme for NextSpark. This minimal theme provides everythi
17
17
  3. Run migrations: `pnpm db:migrate`
18
18
  4. Start development: `pnpm dev`
19
19
 
20
+ Sample users (DevKeyring, Cypress) are for local development only: `pnpm db:seed` loads them; `pnpm db:migrate` alone does not, and neither applies them when `NODE_ENV` is `production`.
21
+
20
22
  ## Project Structure
21
23
 
22
24
  ```