@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.
- package/README.md +2 -0
- package/dist/components/auth/forms/SignupForm.d.ts.map +1 -1
- package/dist/components/auth/forms/SignupForm.js +55 -45
- package/dist/components/billing/InvoicesPagination.js +2 -2
- package/dist/components/dashboard/block-editor/config-panel.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/config-panel.js +3 -2
- package/dist/components/dashboard/block-editor/entity-fields-sidebar.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/entity-fields-sidebar.js +3 -2
- package/dist/components/dashboard/navigation/DynamicNavigation.js +2 -2
- package/dist/components/settings/layouts/SettingsSidebar.d.ts.map +1 -1
- package/dist/components/settings/layouts/SettingsSidebar.js +0 -1
- package/dist/components/teams/CreateTeamButton.js +2 -2
- package/dist/components/teams/CreateTeamDialog.js +1 -1
- package/dist/components/teams/InviteMemberButton.js +3 -3
- package/dist/components/teams/InviteMemberDialog.js +1 -1
- package/dist/components/teams/TeamPendingInvitations.js +2 -2
- package/dist/components/users/tables/UsersTable.d.ts.map +1 -1
- package/dist/components/users/tables/UsersTable.js +3 -4
- package/dist/hooks/useAuthReadiness.d.ts +1 -0
- package/dist/hooks/useAuthReadiness.d.ts.map +1 -1
- package/dist/hooks/useAuthReadiness.js +3 -1
- package/dist/lib/actions/entity.actions.js +2 -2
- package/dist/lib/actions/team.actions.js +2 -2
- package/dist/lib/actions/user.actions.js +4 -4
- package/dist/lib/api/api-error.d.ts +2 -0
- package/dist/lib/api/api-error.d.ts.map +1 -1
- package/dist/lib/api/api-error.js +3 -1
- package/dist/lib/api/auth/dual-auth.d.ts +2 -1
- package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
- package/dist/lib/api/auth/dual-auth.js +25 -6
- package/dist/lib/api/auth/scope-policy.d.ts +6 -3
- package/dist/lib/api/auth/scope-policy.d.ts.map +1 -1
- package/dist/lib/api/auth.d.ts.map +1 -1
- package/dist/lib/api/auth.js +7 -0
- package/dist/lib/api/cors-response.d.ts +3 -2
- package/dist/lib/api/cors-response.d.ts.map +1 -1
- package/dist/lib/api/cors-response.js +8 -6
- package/dist/lib/api/entity/generic-handler.d.ts.map +1 -1
- package/dist/lib/api/entity/generic-handler.js +66 -15
- package/dist/lib/api/entity-handler.js +2 -2
- package/dist/lib/api/helpers.d.ts.map +1 -1
- package/dist/lib/api/helpers.js +9 -24
- package/dist/lib/api/permission-middleware.d.ts.map +1 -1
- package/dist/lib/api/permission-middleware.js +0 -2
- package/dist/lib/api/post-categories.d.ts +29 -0
- package/dist/lib/api/post-categories.d.ts.map +1 -0
- package/dist/lib/api/post-categories.js +33 -0
- package/dist/lib/api/rate-limit.d.ts +8 -0
- package/dist/lib/api/rate-limit.d.ts.map +1 -1
- package/dist/lib/api/rate-limit.js +34 -3
- package/dist/lib/api/request-origin.d.ts.map +1 -1
- package/dist/lib/api/request-origin.js +1 -1
- package/dist/lib/auth/auth-methods.d.ts +5 -2
- package/dist/lib/auth/auth-methods.d.ts.map +1 -1
- package/dist/lib/auth/auth-methods.js +4 -2
- package/dist/lib/auth/authorization-session.d.ts +14 -0
- package/dist/lib/auth/authorization-session.d.ts.map +1 -0
- package/dist/lib/auth/authorization-session.js +10 -0
- package/dist/lib/auth/request-session.d.ts +5 -0
- package/dist/lib/auth/request-session.d.ts.map +1 -1
- package/dist/lib/auth/request-session.js +3 -0
- package/dist/lib/auth/runtime-readiness.d.ts +7 -4
- package/dist/lib/auth/runtime-readiness.d.ts.map +1 -1
- package/dist/lib/auth/runtime-readiness.js +9 -6
- package/dist/lib/auth/sample-accounts.d.ts +18 -0
- package/dist/lib/auth/sample-accounts.d.ts.map +1 -0
- package/dist/lib/auth/sample-accounts.js +25 -0
- package/dist/lib/auth/suspension.d.ts +22 -0
- package/dist/lib/auth/suspension.d.ts.map +1 -0
- package/dist/lib/auth/suspension.js +43 -0
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +20 -17
- package/dist/lib/cache/public-entity-cache.d.ts +21 -0
- package/dist/lib/cache/public-entity-cache.d.ts.map +1 -0
- package/dist/lib/cache/public-entity-cache.js +25 -0
- package/dist/lib/config/app.config.d.ts.map +1 -1
- package/dist/lib/config/app.config.js +6 -7
- package/dist/lib/config/types.d.ts +8 -6
- package/dist/lib/config/types.d.ts.map +1 -1
- package/dist/lib/db-pool.d.ts +31 -0
- package/dist/lib/db-pool.d.ts.map +1 -0
- package/dist/lib/db-pool.js +75 -0
- package/dist/lib/db.d.ts +1 -1
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +11 -14
- package/dist/lib/entities/api-generator.js +5 -5
- package/dist/lib/entities/entity-hooks.d.ts.map +1 -1
- package/dist/lib/entities/entity-hooks.js +4 -0
- package/dist/lib/mcp/errors.d.ts +2 -0
- package/dist/lib/mcp/errors.d.ts.map +1 -1
- package/dist/lib/mcp/errors.js +3 -1
- package/dist/lib/mcp/index.d.ts +1 -1
- package/dist/lib/mcp/index.d.ts.map +1 -1
- package/dist/lib/mcp/index.js +2 -1
- package/dist/lib/oauth/token-refresh.d.ts.map +1 -1
- package/dist/lib/oauth/token-refresh.js +3 -6
- package/dist/lib/permissions/check.d.ts +9 -0
- package/dist/lib/permissions/check.d.ts.map +1 -1
- package/dist/lib/permissions/check.js +11 -1
- package/dist/lib/services/generic-entity.service.d.ts.map +1 -1
- package/dist/lib/services/generic-entity.service.js +41 -7
- package/dist/lib/services/transactional-meta.service.d.ts.map +1 -1
- package/dist/lib/services/transactional-meta.service.js +7 -8
- package/dist/lib/teams/actions.d.ts.map +1 -1
- package/dist/lib/teams/actions.js +0 -1
- package/dist/lib/validation.d.ts +5 -0
- package/dist/lib/validation.d.ts.map +1 -1
- package/dist/lib/validation.js +2 -0
- package/dist/messages/de/billing.json +12 -1
- package/dist/messages/de/common.json +2 -0
- package/dist/messages/de/docs.json +1 -0
- package/dist/messages/de/features.json +16 -4
- package/dist/messages/de/home.json +5 -0
- package/dist/messages/de/index.d.ts +68 -25
- package/dist/messages/de/index.d.ts.map +1 -1
- package/dist/messages/de/index.js +2 -0
- package/dist/messages/de/index.ts +2 -0
- package/dist/messages/de/settings.json +1 -0
- package/dist/messages/de/teams.json +3 -18
- package/dist/messages/en/billing.json +1 -1
- package/dist/messages/en/common.json +2 -0
- package/dist/messages/en/docs.json +1 -0
- package/dist/messages/en/features.json +16 -4
- package/dist/messages/en/home.json +5 -0
- package/dist/messages/en/index.d.ts +58 -25
- package/dist/messages/en/index.d.ts.map +1 -1
- package/dist/messages/en/index.js +2 -0
- package/dist/messages/en/index.ts +2 -0
- package/dist/messages/en/settings.json +1 -0
- package/dist/messages/en/teams.json +3 -18
- package/dist/messages/es/billing.json +1 -1
- package/dist/messages/es/common.json +2 -0
- package/dist/messages/es/docs.json +1 -0
- package/dist/messages/es/features.json +16 -4
- package/dist/messages/es/home.json +5 -0
- package/dist/messages/es/index.d.ts +41 -8
- package/dist/messages/es/index.d.ts.map +1 -1
- package/dist/messages/es/index.js +2 -0
- package/dist/messages/es/index.ts +2 -0
- package/dist/messages/es/settings.json +1 -0
- package/dist/messages/es/teams.json +3 -0
- package/dist/messages/fr/billing.json +12 -1
- package/dist/messages/fr/common.json +2 -0
- package/dist/messages/fr/docs.json +1 -0
- package/dist/messages/fr/features.json +16 -4
- package/dist/messages/fr/home.json +5 -0
- package/dist/messages/fr/index.d.ts +68 -25
- package/dist/messages/fr/index.d.ts.map +1 -1
- package/dist/messages/fr/index.js +2 -0
- package/dist/messages/fr/index.ts +2 -0
- package/dist/messages/fr/settings.json +1 -0
- package/dist/messages/fr/teams.json +3 -18
- package/dist/messages/it/billing.json +12 -1
- package/dist/messages/it/common.json +2 -0
- package/dist/messages/it/docs.json +1 -0
- package/dist/messages/it/features.json +16 -4
- package/dist/messages/it/home.json +5 -0
- package/dist/messages/it/index.d.ts +68 -25
- package/dist/messages/it/index.d.ts.map +1 -1
- package/dist/messages/it/index.js +2 -0
- package/dist/messages/it/index.ts +2 -0
- package/dist/messages/it/settings.json +1 -0
- package/dist/messages/it/teams.json +3 -18
- package/dist/messages/pt/billing.json +12 -1
- package/dist/messages/pt/common.json +2 -0
- package/dist/messages/pt/docs.json +1 -0
- package/dist/messages/pt/features.json +16 -4
- package/dist/messages/pt/home.json +5 -0
- package/dist/messages/pt/index.d.ts +68 -25
- package/dist/messages/pt/index.d.ts.map +1 -1
- package/dist/messages/pt/index.js +2 -0
- package/dist/messages/pt/index.ts +2 -0
- package/dist/messages/pt/settings.json +1 -0
- package/dist/messages/pt/teams.json +3 -18
- package/dist/migrations/029_sample_accounts_outside_development.sql +55 -0
- package/dist/migrations/030_taxonomies_team_writes.sql +29 -0
- package/dist/migrations/031_users_role_suspended.sql +41 -0
- package/dist/migrations/090_sample_data.sql +10 -0
- package/dist/routes/(auth)/signup/page.cc.d.ts +3 -1
- package/dist/routes/(auth)/signup/page.cc.d.ts.map +1 -1
- package/dist/routes/(auth)/signup/page.cc.js +2 -2
- package/dist/routes/(auth)/signup/page.d.ts +3 -1
- package/dist/routes/(auth)/signup/page.d.ts.map +1 -1
- package/dist/routes/(auth)/signup/page.js +4 -3
- package/dist/routes/_internal/area-access.js +2 -2
- package/dist/routes/_internal/entity-permission-layout.d.ts.map +1 -1
- package/dist/routes/_internal/entity-permission-layout.js +0 -7
- package/dist/routes/api/devtools/config/entities/route.js +2 -2
- package/dist/routes/api/devtools/config/theme/route.js +2 -2
- package/dist/routes/api/devtools/tests/[...path]/route.js +2 -2
- package/dist/routes/api/devtools/tests/route.js +2 -2
- package/dist/routes/api/mcp/route.d.ts.map +1 -1
- package/dist/routes/api/mcp/route.js +7 -1
- package/dist/routes/api/superadmin/subscriptions/route.js +2 -2
- package/dist/routes/api/superadmin/teams/[teamId]/route.js +2 -2
- package/dist/routes/api/superadmin/teams/route.js +2 -2
- package/dist/routes/api/superadmin/users/[userId]/route.d.ts +5 -3
- package/dist/routes/api/superadmin/users/[userId]/route.d.ts.map +1 -1
- package/dist/routes/api/superadmin/users/[userId]/route.js +46 -23
- package/dist/routes/api/superadmin/users/route.js +2 -2
- package/dist/routes/api/user/delete-account/route.d.ts.map +1 -1
- package/dist/routes/api/user/delete-account/route.js +2 -3
- package/dist/routes/api/user/plan-flags/route.d.ts.map +1 -1
- package/dist/routes/api/user/plan-flags/route.js +5 -5
- package/dist/routes/api/user/profile/route.js +3 -3
- package/dist/routes/api/v1/auth/signup-with-invite/route.d.ts.map +1 -1
- package/dist/routes/api/v1/auth/signup-with-invite/route.js +80 -44
- package/dist/routes/api/v1/media/upload/route.d.ts +2 -1
- package/dist/routes/api/v1/media/upload/route.d.ts.map +1 -1
- package/dist/routes/api/v1/media/upload/route.js +10 -2
- package/dist/routes/api/v1/post-categories/[id]/route.d.ts.map +1 -1
- package/dist/routes/api/v1/post-categories/[id]/route.js +39 -34
- package/dist/routes/api/v1/post-categories/route.d.ts.map +1 -1
- package/dist/routes/api/v1/post-categories/route.js +18 -14
- package/dist/routes/api/v1/team-invitations/route.d.ts.map +1 -1
- package/dist/routes/api/v1/team-invitations/route.js +12 -5
- package/dist/routes/api/v1/teams/[teamId]/members/route.d.ts.map +1 -1
- package/dist/routes/api/v1/teams/[teamId]/members/route.js +12 -10
- package/dist/routes/api/v1/teams/switch/route.d.ts.map +1 -1
- package/dist/routes/api/v1/teams/switch/route.js +4 -1
- package/dist/routes/dashboard/settings/password/page.d.ts.map +1 -1
- package/dist/routes/dashboard/settings/password/page.js +10 -12
- package/dist/routes/devtools/config/page.js +1 -1
- package/dist/routes/devtools/page.js +1 -1
- package/dist/routes/devtools/scheduled-actions/page.js +1 -1
- package/dist/routes/devtools/tests/[[...path]]/page.js +1 -1
- package/dist/routes/superadmin/users/page.js +2 -2
- package/dist/templates/ci-workflows/github/cypress-regression.yml +1 -1
- package/dist/templates/ci-workflows/github/cypress-smoke.yml +2 -1
- package/dist/templates/next.config.mjs +11 -31
- package/dist/templates/projects/blog/package.json +1 -1
- package/dist/templates/projects/crm/package.json +1 -1
- package/dist/templates/projects/productivity/package.json +1 -1
- package/dist/templates/projects/starter/README.md +2 -0
- package/dist/templates/projects/starter/config/app.config.ts +8 -0
- package/dist/templates/projects/starter/docs/public/01-overview/01-introduction.md +2 -0
- package/dist/templates/projects/starter/templates/dashboard/analytics/layout.tsx +14 -0
- package/dist/templates/proxy.ts +18 -8
- package/dist/templates/public/uploads/README.md +5 -94
- package/migrations/029_sample_accounts_outside_development.sql +55 -0
- package/migrations/030_taxonomies_team_writes.sql +29 -0
- package/migrations/031_users_role_suspended.sql +41 -0
- package/migrations/090_sample_data.sql +10 -0
- package/package.json +3 -3
- package/scripts/build/registry/__tests__/build-output.test.mjs +1 -1
- package/scripts/build/registry/__tests__/project-mode-root-first.test.mjs +6 -6
- package/scripts/build/registry/__tests__/write-places.test.mjs +1 -1
- package/scripts/build/registry/contracts/__tests__/contracts-wire.test.mjs +1 -1
- package/scripts/build/registry/contracts/__tests__/contracts.test.mjs +1 -1
- package/scripts/build/registry/host/README.md +2 -2
- package/scripts/build/registry/host/__tests__/facade-emitter.test.mjs +14 -13
- package/scripts/build/registry/host/__tests__/host-mode.test.mjs +1 -1
- package/scripts/build/registry/host/__tests__/host-project-composition.test.mjs +1 -1
- package/scripts/build/registry/host/__tests__/host-project.test.mjs +2 -2
- package/scripts/build/registry/host/__tests__/host-proxy-areas.test.mjs +99 -1
- package/scripts/build/registry/host/__tests__/host-robustness.test.mjs +1 -1
- package/scripts/build/registry/host/facade-emitter.mjs +1 -1
- package/scripts/build/registry/host/next-route-exports.json +2 -2
- package/scripts/build/registry/host/proxy-areas.mjs +92 -2
- package/scripts/db/run-migrations.mjs +30 -10
- package/scripts/db/sample-data.mjs +44 -0
- package/scripts/db/verify-theme-migrations.mjs +7 -1
- package/templates/ci-workflows/github/cypress-regression.yml +1 -1
- package/templates/ci-workflows/github/cypress-smoke.yml +2 -1
- package/templates/next.config.mjs +11 -31
- package/templates/projects/blog/package.json +1 -1
- package/templates/projects/crm/package.json +1 -1
- package/templates/projects/productivity/package.json +1 -1
- package/templates/projects/starter/README.md +2 -0
- package/templates/projects/starter/config/app.config.ts +8 -0
- package/templates/projects/starter/docs/public/01-overview/01-introduction.md +2 -0
- package/templates/projects/starter/templates/dashboard/analytics/layout.tsx +14 -0
- package/templates/proxy.ts +18 -8
- package/templates/public/uploads/README.md +5 -94
- package/tests/jest/__mocks__/@nextsparkjs/registries/api-presets-registry.ts +2 -0
- package/tests/jest/__mocks__/@nextsparkjs/registries/mcp-registry.ts +2 -0
- package/tests/jest/__mocks__/@nextsparkjs/registries/permissions-registry.ts +5 -4
- package/dist/templates/projects/starter/public/brand/.gitkeep +0 -0
- package/dist/templates/public/uploads/.gitignore +0 -6
- package/dist/templates/public/uploads/temp/.gitkeep +0 -1
- package/templates/projects/starter/public/brand/.gitkeep +0 -0
- package/templates/public/uploads/.gitignore +0 -6
- 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.
|
|
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.
|
|
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.
|
|
4
|
-
"supportedNext": "~16.3.
|
|
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
|
|
55
|
-
return
|
|
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.
|
|
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 });
|
|
@@ -59,7 +59,8 @@ jobs:
|
|
|
59
59
|
- name: Setup database
|
|
60
60
|
env:
|
|
61
61
|
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
|
|
62
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
//
|
|
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.
|
|
@@ -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
|
```
|