@nextsparkjs/core 0.1.0-beta.196 → 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 (136) 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/dashboard/block-editor/config-panel.d.ts.map +1 -1
  5. package/dist/components/dashboard/block-editor/config-panel.js +3 -2
  6. package/dist/components/dashboard/block-editor/entity-fields-sidebar.d.ts.map +1 -1
  7. package/dist/components/dashboard/block-editor/entity-fields-sidebar.js +3 -2
  8. package/dist/components/users/tables/UsersTable.d.ts.map +1 -1
  9. package/dist/components/users/tables/UsersTable.js +3 -4
  10. package/dist/hooks/useAuthReadiness.d.ts +1 -0
  11. package/dist/hooks/useAuthReadiness.d.ts.map +1 -1
  12. package/dist/hooks/useAuthReadiness.js +3 -1
  13. package/dist/lib/actions/entity.actions.js +2 -2
  14. package/dist/lib/actions/team.actions.js +2 -2
  15. package/dist/lib/actions/user.actions.js +4 -4
  16. package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
  17. package/dist/lib/api/auth/dual-auth.js +3 -2
  18. package/dist/lib/api/auth.d.ts.map +1 -1
  19. package/dist/lib/api/auth.js +7 -0
  20. package/dist/lib/api/entity/generic-handler.d.ts.map +1 -1
  21. package/dist/lib/api/entity/generic-handler.js +66 -12
  22. package/dist/lib/api/entity-handler.js +2 -2
  23. package/dist/lib/api/helpers.d.ts.map +1 -1
  24. package/dist/lib/api/helpers.js +4 -2
  25. package/dist/lib/api/post-categories.d.ts +29 -0
  26. package/dist/lib/api/post-categories.d.ts.map +1 -0
  27. package/dist/lib/api/post-categories.js +33 -0
  28. package/dist/lib/auth/auth-methods.d.ts +5 -2
  29. package/dist/lib/auth/auth-methods.d.ts.map +1 -1
  30. package/dist/lib/auth/auth-methods.js +4 -2
  31. package/dist/lib/auth/authorization-session.d.ts +14 -0
  32. package/dist/lib/auth/authorization-session.d.ts.map +1 -0
  33. package/dist/lib/auth/authorization-session.js +10 -0
  34. package/dist/lib/auth/request-session.d.ts +5 -0
  35. package/dist/lib/auth/request-session.d.ts.map +1 -1
  36. package/dist/lib/auth/request-session.js +3 -0
  37. package/dist/lib/auth/runtime-readiness.d.ts +7 -4
  38. package/dist/lib/auth/runtime-readiness.d.ts.map +1 -1
  39. package/dist/lib/auth/runtime-readiness.js +7 -6
  40. package/dist/lib/auth/sample-accounts.d.ts +2 -1
  41. package/dist/lib/auth/sample-accounts.d.ts.map +1 -1
  42. package/dist/lib/auth/sample-accounts.js +4 -1
  43. package/dist/lib/auth/suspension.d.ts +22 -0
  44. package/dist/lib/auth/suspension.d.ts.map +1 -0
  45. package/dist/lib/auth/suspension.js +43 -0
  46. package/dist/lib/auth.d.ts.map +1 -1
  47. package/dist/lib/auth.js +15 -15
  48. package/dist/lib/config/app.config.d.ts.map +1 -1
  49. package/dist/lib/config/app.config.js +6 -7
  50. package/dist/lib/config/types.d.ts +8 -6
  51. package/dist/lib/config/types.d.ts.map +1 -1
  52. package/dist/lib/db-pool.d.ts +31 -0
  53. package/dist/lib/db-pool.d.ts.map +1 -0
  54. package/dist/lib/db-pool.js +75 -0
  55. package/dist/lib/db.d.ts +1 -1
  56. package/dist/lib/db.d.ts.map +1 -1
  57. package/dist/lib/db.js +11 -14
  58. package/dist/lib/entities/api-generator.js +5 -5
  59. package/dist/lib/oauth/token-refresh.d.ts.map +1 -1
  60. package/dist/lib/oauth/token-refresh.js +3 -6
  61. package/dist/lib/permissions/check.d.ts +9 -0
  62. package/dist/lib/permissions/check.d.ts.map +1 -1
  63. package/dist/lib/permissions/check.js +11 -1
  64. package/dist/lib/services/generic-entity.service.d.ts.map +1 -1
  65. package/dist/lib/services/generic-entity.service.js +39 -7
  66. package/dist/lib/services/transactional-meta.service.d.ts.map +1 -1
  67. package/dist/lib/services/transactional-meta.service.js +7 -8
  68. package/dist/lib/validation.d.ts +5 -0
  69. package/dist/lib/validation.d.ts.map +1 -1
  70. package/dist/lib/validation.js +2 -0
  71. package/dist/migrations/030_taxonomies_team_writes.sql +29 -0
  72. package/dist/migrations/031_users_role_suspended.sql +41 -0
  73. package/dist/routes/(auth)/signup/page.cc.d.ts +3 -1
  74. package/dist/routes/(auth)/signup/page.cc.d.ts.map +1 -1
  75. package/dist/routes/(auth)/signup/page.cc.js +2 -2
  76. package/dist/routes/(auth)/signup/page.d.ts +3 -1
  77. package/dist/routes/(auth)/signup/page.d.ts.map +1 -1
  78. package/dist/routes/(auth)/signup/page.js +4 -3
  79. package/dist/routes/_internal/area-access.js +2 -2
  80. package/dist/routes/api/devtools/config/entities/route.js +2 -2
  81. package/dist/routes/api/devtools/config/theme/route.js +2 -2
  82. package/dist/routes/api/devtools/tests/[...path]/route.js +2 -2
  83. package/dist/routes/api/devtools/tests/route.js +2 -2
  84. package/dist/routes/api/superadmin/subscriptions/route.js +2 -2
  85. package/dist/routes/api/superadmin/teams/[teamId]/route.js +2 -2
  86. package/dist/routes/api/superadmin/teams/route.js +2 -2
  87. package/dist/routes/api/superadmin/users/[userId]/route.d.ts +5 -3
  88. package/dist/routes/api/superadmin/users/[userId]/route.d.ts.map +1 -1
  89. package/dist/routes/api/superadmin/users/[userId]/route.js +46 -23
  90. package/dist/routes/api/superadmin/users/route.js +2 -2
  91. package/dist/routes/api/user/delete-account/route.d.ts.map +1 -1
  92. package/dist/routes/api/user/delete-account/route.js +2 -3
  93. package/dist/routes/api/user/plan-flags/route.d.ts.map +1 -1
  94. package/dist/routes/api/user/plan-flags/route.js +5 -5
  95. package/dist/routes/api/user/profile/route.js +3 -3
  96. package/dist/routes/api/v1/auth/signup-with-invite/route.d.ts.map +1 -1
  97. package/dist/routes/api/v1/auth/signup-with-invite/route.js +80 -44
  98. package/dist/routes/api/v1/post-categories/[id]/route.d.ts.map +1 -1
  99. package/dist/routes/api/v1/post-categories/[id]/route.js +39 -34
  100. package/dist/routes/api/v1/post-categories/route.d.ts.map +1 -1
  101. package/dist/routes/api/v1/post-categories/route.js +18 -14
  102. package/dist/routes/api/v1/team-invitations/route.d.ts.map +1 -1
  103. package/dist/routes/api/v1/team-invitations/route.js +12 -5
  104. package/dist/routes/superadmin/users/page.js +2 -2
  105. package/dist/templates/next.config.mjs +11 -31
  106. package/dist/templates/projects/blog/package.json +1 -1
  107. package/dist/templates/projects/crm/package.json +1 -1
  108. package/dist/templates/projects/productivity/package.json +1 -1
  109. package/dist/templates/projects/starter/config/app.config.ts +8 -0
  110. package/dist/templates/proxy.ts +6 -3
  111. package/migrations/030_taxonomies_team_writes.sql +29 -0
  112. package/migrations/031_users_role_suspended.sql +41 -0
  113. package/package.json +3 -3
  114. package/scripts/build/registry/__tests__/build-output.test.mjs +1 -1
  115. package/scripts/build/registry/__tests__/project-mode-root-first.test.mjs +6 -6
  116. package/scripts/build/registry/__tests__/write-places.test.mjs +1 -1
  117. package/scripts/build/registry/contracts/__tests__/contracts-wire.test.mjs +1 -1
  118. package/scripts/build/registry/contracts/__tests__/contracts.test.mjs +1 -1
  119. package/scripts/build/registry/host/README.md +2 -2
  120. package/scripts/build/registry/host/__tests__/facade-emitter.test.mjs +14 -13
  121. package/scripts/build/registry/host/__tests__/host-mode.test.mjs +1 -1
  122. package/scripts/build/registry/host/__tests__/host-project-composition.test.mjs +1 -1
  123. package/scripts/build/registry/host/__tests__/host-project.test.mjs +2 -2
  124. package/scripts/build/registry/host/__tests__/host-proxy-areas.test.mjs +29 -2
  125. package/scripts/build/registry/host/__tests__/host-robustness.test.mjs +1 -1
  126. package/scripts/build/registry/host/facade-emitter.mjs +1 -1
  127. package/scripts/build/registry/host/next-route-exports.json +2 -2
  128. package/scripts/build/registry/host/proxy-areas.mjs +29 -2
  129. package/scripts/db/verify-theme-migrations.mjs +1 -1
  130. package/templates/next.config.mjs +11 -31
  131. package/templates/projects/blog/package.json +1 -1
  132. package/templates/projects/crm/package.json +1 -1
  133. package/templates/projects/productivity/package.json +1 -1
  134. package/templates/projects/starter/config/app.config.ts +8 -0
  135. package/templates/proxy.ts +6 -3
  136. package/tests/jest/__mocks__/@nextsparkjs/registries/permissions-registry.ts +5 -4
@@ -124,11 +124,12 @@ test("page and layout exports include Next.js's TS-plugin ALLOWED_EXPORTS (excep
124
124
  }
125
125
  })
126
126
 
127
- test('metadata route loader still filters dynamicParams/generateSitemaps out of its re-exports', async () => {
128
- // The table forbids dynamicParams and generateStaticParams on metadata routes because the
129
- // loader either drops them or declares its own; detect a Next.js change to that code.
127
+ test('metadata route loader still filters generateSitemaps out of its re-exports', async () => {
128
+ // The table forbids dynamicParams and generateStaticParams on metadata routes: the loader
129
+ // declares its own generateStaticParams, and up to 16.3.7 it dropped dynamicParams (16.3.8
130
+ // re-exports it; the table keeps refusing it). Detect a Next.js change to that code.
130
131
  const loader = await readFile(requireFromCore.resolve('next/dist/build/webpack/loaders/next-metadata-route-loader'), 'utf8')
131
- assert.match(loader, /name !== 'default' && name !== 'generateSitemaps' && name !== 'dynamicParams'/)
132
+ assert.match(loader, /exportNames\.filter\(\(name\)=>name !== 'default' && name !== 'generateSitemaps'\);/)
132
133
  assert.match(loader, /export async function generateStaticParams/)
133
134
  })
134
135
 
@@ -595,14 +596,14 @@ test('an aliased magic export is forwarded under its source local name', async (
595
596
  assert.match(content, /^export \{ listSlugs as generateStaticParams \}$/m)
596
597
  })
597
598
 
598
- // --- fix round 3: ~baseline version range (baseline 16.3.6 since 0.1.0-beta.194) ---
599
+ // --- fix round 3: ~baseline version range (baseline 16.3.8 since 0.1.0-beta.197) ---
599
600
 
600
601
  test('isSupportedNextVersion accepts ~baseline: same minor, stable patch >= baseline', () => {
601
- assert.equal(ROUTE_EXPORT_TABLE.next, '16.3.6')
602
- for (const version of ['16.3.6', '16.3.7', '16.3.9', '16.3.10', '16.3.123', '16.3.6+build.1', '16.3.7+sha.abc-1']) assert.equal(isSupportedNextVersion(version), true, version)
602
+ assert.equal(ROUTE_EXPORT_TABLE.next, '16.3.8')
603
+ for (const version of ['16.3.8', '16.3.9', '16.3.10', '16.3.123', '16.3.8+build.1', '16.3.9+sha.abc-1']) assert.equal(isSupportedNextVersion(version), true, version)
603
604
  for (const version of [
604
- '16.3.5', '16.3.4', '16.3.0', '16.4.0', '16.2.9', '17.0.0', '15.3.6', '16.3.7-canary.0', '16.3.6-rc.1', '16.3.7-canary.0+build.1',
605
- '16.3.5+build.1', '16.3.6+', '16.3.6+bad..meta', '16.3', 'latest', '', undefined,
605
+ '16.3.7', '16.3.6', '16.3.5', '16.3.4', '16.3.0', '16.4.0', '16.2.9', '17.0.0', '15.3.6', '16.3.7-canary.0', '16.3.8-rc.1', '16.3.7-canary.0+build.1',
606
+ '16.3.5+build.1', '16.3.8+', '16.3.8+bad..meta', '16.3', 'latest', '', undefined,
606
607
  ]) {
607
608
  assert.equal(isSupportedNextVersion(version), false, String(version))
608
609
  }
@@ -625,7 +626,7 @@ test('a later 16.3 patch is accepted and its own schema internal is used', async
625
626
  }
626
627
  })
627
628
 
628
- for (const version of ['16.3.5', '16.4.0', '17.0.0', '16.3.6-canary.0']) {
629
+ for (const version of ['16.3.5', '16.4.0', '17.0.0', '16.3.8-canary.0']) {
629
630
  test(`${DIAGNOSTICS.UNSUPPORTED_NEXT_VERSION}: next@${version} is refused`, async () => {
630
631
  const dir = await fakeNextProject(version, "exports.AppSegmentConfigSchemaKeys = []\nexports.parseAppSegmentConfig = () => ({})\n")
631
632
  try {
@@ -634,7 +635,7 @@ for (const version of ['16.3.5', '16.4.0', '17.0.0', '16.3.6-canary.0']) {
634
635
  error => {
635
636
  assert.deepEqual(error.diagnostics.map(d => d.code), [DIAGNOSTICS.UNSUPPORTED_NEXT_VERSION])
636
637
  assert.match(error.message, new RegExp(`next@${version.replace(/\./g, '\\.')}`))
637
- assert.match(error.message, /next@~16\.3\.6/)
638
+ assert.match(error.message, /next@~16\.3\.8/)
638
639
  return true
639
640
  }
640
641
  )
@@ -646,9 +647,9 @@ for (const version of ['16.3.5', '16.4.0', '17.0.0', '16.3.6-canary.0']) {
646
647
 
647
648
  // --- fix round 4: build metadata ----------------------------------------------
648
649
 
649
- test('a Next.js version with build metadata (16.3.6+build.1) is accepted end to end', async () => {
650
+ test('a Next.js version with build metadata (16.3.8+build.1) is accepted end to end', async () => {
650
651
  const marker = "exports.AppSegmentConfigSchemaKeys = []\nexports.parseAppSegmentConfig = () => ({})\n"
651
- const dir = await fakeNextProject('16.3.6+build.1', marker)
652
+ const dir = await fakeNextProject('16.3.8+build.1', marker)
652
653
  try {
653
654
  assert.equal(loadNextSegmentConfig(dir).error, undefined)
654
655
  const { content } = await emitFacade({ kind: 'page', target: 'a/page.tsx', specifier: '@/a', file: FILE, source: PAGE_WITH_CONFIG, projectRoot: dir })
@@ -17,7 +17,7 @@ const PREPARE_CLI = join(CORE_ROOT, 'scripts/build/registry/host/prepare-cli.mjs
17
17
  function project({ legacyApp = false } = {}) {
18
18
  const root = mkdtempSync(join(tmpdir(), 'nextspark-host-mode-'))
19
19
  write(root, 'nextspark.config.ts', 'export default { plugins: [] }\n')
20
- write(root, 'package.json', JSON.stringify({ name: 'host-mode', dependencies: { next: '16.3.6' } }))
20
+ write(root, 'package.json', JSON.stringify({ name: 'host-mode', dependencies: { next: '16.3.8' } }))
21
21
  write(root, 'templates/pricing/page.tsx', PAGE('Pricing'))
22
22
  if (legacyApp) write(root, 'src/app/layout.tsx', 'export default function Layout({ children }) { return children }\n')
23
23
  return { root, cleanup: () => rmSync(root, { recursive: true, force: true }) }
@@ -36,7 +36,7 @@ const LAYOUT = 'export default function Layout({ children }: { children: React.R
36
36
  function project(extra = {}) {
37
37
  const root = mkdtempSync(join(tmpdir(), 'nextspark-host-composition-'))
38
38
  write(root, 'nextspark.config.ts', "export default { plugins: [], billing: { webhookExtensions: { stripe: './lib/billing/stripe-webhook-extensions' } } }\n")
39
- write(root, 'package.json', JSON.stringify({ name: 'composition', dependencies: { next: '16.3.6' } }))
39
+ write(root, 'package.json', JSON.stringify({ name: 'composition', dependencies: { next: '16.3.8' } }))
40
40
  write(root, 'config/theme.config.ts', "export const compositionThemeConfig = { name: 'composition' }\n")
41
41
  write(root, 'styles/globals.css', '@import "tailwindcss";\n')
42
42
  write(root, 'lib/billing/stripe-webhook-extensions.ts', 'export const stripeWebhookExtensions = {}\n')
@@ -16,7 +16,7 @@ const PREPARE_CLI = join(CORE_ROOT, 'scripts/build/registry/host/prepare-cli.mjs
16
16
  function project() {
17
17
  const root = mkdtempSync(join(tmpdir(), 'nextspark-host-project-'))
18
18
  write(root, 'nextspark.config.ts', 'export default { plugins: [] }\n')
19
- write(root, 'package.json', JSON.stringify({ name: 'host-project', dependencies: { next: '16.3.6' } }))
19
+ write(root, 'package.json', JSON.stringify({ name: 'host-project', dependencies: { next: '16.3.8' } }))
20
20
  write(root, 'templates/pricing/page.tsx', PAGE('Pricing'))
21
21
  write(root, 'api/ping/route.ts', ROUTE)
22
22
  return { root, cleanup: () => rmSync(root, { recursive: true, force: true }) }
@@ -41,7 +41,7 @@ test('a real project: the registry build is staged and published with the host,
41
41
 
42
42
  const record = readGeneration(root)
43
43
  assert.equal(Object.keys(record.files).length, first.files.length)
44
- assert.ok(record.versions.core && record.versions.next === '16.3.6')
44
+ assert.ok(record.versions.core && record.versions.next === '16.3.8')
45
45
  assert.ok(record.inputs.files['nextspark.config.ts'] && record.inputs.files['templates/pricing/page.tsx'])
46
46
  assert.deepEqual(await checkHost(config), { ok: true, problems: [] })
47
47
 
@@ -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, PROXY_SESSION_WARNING, missingProxyAreas, withoutComments, proxyAreaNotice, proxyAreaNotices, proxySessionNotice } 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
 
@@ -83,7 +83,7 @@ test('a proxy that asks its own /api/auth/get-session over HTTP is warned about,
83
83
  assert.equal(notice.code, PROXY_SESSION_WARNING)
84
84
  assert.match(notice.message, /^src\/proxy\.ts checks the session by fetching \/api\/auth\/get-session/)
85
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 \} \}\)/)
86
+ assert.match(notice.message, /auth\.api\.getSession\(\{ headers: new Headers\(\{ cookie: request\.headers\.get\('cookie'\) \|\| '' \}\), query: \{ disableRefresh: true, disableCookieCache: true \} \}\)/)
87
87
  assert.equal(proxySessionNotice('src/proxy.ts', readFileSync(join(CORE_ROOT, 'templates/proxy.ts'), 'utf8')), null)
88
88
  assert.equal(proxySessionNotice('src/proxy.ts', "// was: betterFetch('/api/auth/get-session')\nexport function proxy() {}"), null)
89
89
  assert.equal(proxySessionNotice('src/proxy.ts', "export { proxy } from '@nextsparkjs/core/templates/proxy'"), null)
@@ -138,3 +138,30 @@ test("prepare's prediction carries the warning among its notices", async () => {
138
138
  rmSync(project, { recursive: true, force: true })
139
139
  }
140
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)",
@@ -92,7 +92,7 @@ export const PROXY_SESSION_WARNING = 'NS_PROXY_SESSION_OVER_HTTP'
92
92
  /** What to write instead of the HTTP session check; migrate in the CLI says the same. */
93
93
  export const PROXY_SESSION_FIX =
94
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 } }), " +
95
+ "auth.api.getSession({ headers: new Headers({ cookie: request.headers.get('cookie') || '' }), query: { disableRefresh: true, disableCookieCache: true } }), " +
96
96
  'which returns the session itself (or null), not { data }'
97
97
 
98
98
  /** The warning for a proxy whose code (not a comment) asks /api/auth/get-session over HTTP, or null. */
@@ -109,11 +109,38 @@ export function proxySessionNotice(file, source) {
109
109
  }
110
110
  }
111
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
+
112
139
  /** The warnings for the proxy Next would load in `projectRoot` (the first of PROXY_FILES that exists). */
113
140
  export function proxyAreaNotices(projectRoot) {
114
141
  if (!projectRoot) return []
115
142
  const file = PROXY_FILES.find(candidate => existsSync(join(projectRoot, candidate)))
116
143
  if (!file) return []
117
144
  const source = readFileSync(join(projectRoot, file), 'utf8')
118
- return [proxyAreaNotice(file, source), proxySessionNotice(file, source)].filter(Boolean)
145
+ return [proxyAreaNotice(file, source), proxySessionNotice(file, source), proxySessionCachedNotice(file, source)].filter(Boolean)
119
146
  }
@@ -157,7 +157,7 @@ async function main() {
157
157
  extracted = fs.mkdtempSync(path.join(os.tmpdir(), `nextspark-template-${theme}-`));
158
158
  fs.cpSync(located.sourceDir, extracted, { recursive: true });
159
159
  fs.writeFileSync(path.join(extracted, 'nextspark.config.ts'), `export default { plugins: [], template: { name: ${JSON.stringify(theme)}, version: 'test' } }\n`);
160
- 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));
161
161
  runDir = extracted;
162
162
  }
163
163
 
@@ -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"
@@ -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:
@@ -189,14 +189,17 @@ function redirectAccessDenied(request: NextRequest): NextResponse {
189
189
  * Nothing here leaves the process, so no forwarded scheme or host can point
190
190
  * the check anywhere. `disableRefresh`: this response cannot carry the renewed
191
191
  * session cookie, so the rolling renewal is left to the auth route
192
- * (lib/auth/session-refresh).
192
+ * (lib/auth/session-refresh). `disableCookieCache`: the session comes from the
193
+ * database, not the signed session_data cookie, so a sign-out, a revoked
194
+ * session or a role change counts at once (lib/auth/authorization-session).
195
+ * A suspended account's session is no session (lib/auth/suspension).
193
196
  */
194
197
  async function getSession(request: NextRequest): Promise<Session | null> {
195
198
  const session = await auth.api.getSession({
196
199
  headers: new Headers({ cookie: request.headers.get('cookie') || '' }),
197
- query: { disableRefresh: true },
200
+ query: { disableRefresh: true, disableCookieCache: true },
198
201
  })
199
- return session as Session | null
202
+ return session && (session.user as { role?: unknown }).role !== 'suspended' ? (session as Session) : null
200
203
  }
201
204
 
202
205
  /**
@@ -37,12 +37,13 @@ export const TEAM_PERMISSIONS_BY_ROLE: Record<string, string[]> = {
37
37
  viewer: ['team.view'],
38
38
  }
39
39
 
40
- // Entity permissions used by service tests (mirrors the default theme's customers entity)
40
+ // Entity permissions used by service tests (mirrors the default theme's customers entity, and the starter's
41
+ // posts entity, whose `publish` action is owner/admin only)
41
42
  const ENTITY_PERMISSIONS_BY_ROLE: Record<string, string[]> = {
42
- owner: ['customers.create', 'customers.read', 'customers.list', 'customers.update', 'customers.delete'],
43
- admin: ['customers.create', 'customers.read', 'customers.list', 'customers.update'],
43
+ owner: ['customers.create', 'customers.read', 'customers.list', 'customers.update', 'customers.delete', 'posts.create', 'posts.update', 'posts.publish'],
44
+ admin: ['customers.create', 'customers.read', 'customers.list', 'customers.update', 'posts.create', 'posts.update', 'posts.publish'],
44
45
  editor: ['customers.read', 'customers.list'],
45
- member: ['customers.read', 'customers.list'],
46
+ member: ['customers.read', 'customers.list', 'posts.create', 'posts.update'],
46
47
  viewer: [],
47
48
  }
48
49