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

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 (168) hide show
  1. package/dist/components/billing/InvoicesPagination.js +2 -2
  2. package/dist/components/dashboard/navigation/DynamicNavigation.js +2 -2
  3. package/dist/components/settings/layouts/SettingsSidebar.d.ts.map +1 -1
  4. package/dist/components/settings/layouts/SettingsSidebar.js +0 -1
  5. package/dist/components/teams/CreateTeamButton.js +2 -2
  6. package/dist/components/teams/CreateTeamDialog.js +1 -1
  7. package/dist/components/teams/InviteMemberButton.js +3 -3
  8. package/dist/components/teams/InviteMemberDialog.js +1 -1
  9. package/dist/components/teams/TeamPendingInvitations.js +2 -2
  10. package/dist/lib/api/api-error.d.ts +2 -0
  11. package/dist/lib/api/api-error.d.ts.map +1 -1
  12. package/dist/lib/api/api-error.js +3 -1
  13. package/dist/lib/api/auth/dual-auth.d.ts +2 -1
  14. package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
  15. package/dist/lib/api/auth/dual-auth.js +22 -4
  16. package/dist/lib/api/auth/scope-policy.d.ts +6 -3
  17. package/dist/lib/api/auth/scope-policy.d.ts.map +1 -1
  18. package/dist/lib/api/cors-response.d.ts +3 -2
  19. package/dist/lib/api/cors-response.d.ts.map +1 -1
  20. package/dist/lib/api/cors-response.js +8 -6
  21. package/dist/lib/api/entity/generic-handler.d.ts.map +1 -1
  22. package/dist/lib/api/entity/generic-handler.js +0 -3
  23. package/dist/lib/api/helpers.d.ts.map +1 -1
  24. package/dist/lib/api/helpers.js +5 -22
  25. package/dist/lib/api/permission-middleware.d.ts.map +1 -1
  26. package/dist/lib/api/permission-middleware.js +0 -2
  27. package/dist/lib/api/rate-limit.d.ts +8 -0
  28. package/dist/lib/api/rate-limit.d.ts.map +1 -1
  29. package/dist/lib/api/rate-limit.js +34 -3
  30. package/dist/lib/api/request-origin.d.ts.map +1 -1
  31. package/dist/lib/api/request-origin.js +1 -1
  32. package/dist/lib/auth/runtime-readiness.d.ts.map +1 -1
  33. package/dist/lib/auth/runtime-readiness.js +2 -0
  34. package/dist/lib/auth/sample-accounts.d.ts +17 -0
  35. package/dist/lib/auth/sample-accounts.d.ts.map +1 -0
  36. package/dist/lib/auth/sample-accounts.js +22 -0
  37. package/dist/lib/auth.d.ts.map +1 -1
  38. package/dist/lib/auth.js +6 -3
  39. package/dist/lib/cache/public-entity-cache.d.ts +21 -0
  40. package/dist/lib/cache/public-entity-cache.d.ts.map +1 -0
  41. package/dist/lib/cache/public-entity-cache.js +25 -0
  42. package/dist/lib/entities/entity-hooks.d.ts.map +1 -1
  43. package/dist/lib/entities/entity-hooks.js +4 -0
  44. package/dist/lib/mcp/errors.d.ts +2 -0
  45. package/dist/lib/mcp/errors.d.ts.map +1 -1
  46. package/dist/lib/mcp/errors.js +3 -1
  47. package/dist/lib/mcp/index.d.ts +1 -1
  48. package/dist/lib/mcp/index.d.ts.map +1 -1
  49. package/dist/lib/mcp/index.js +2 -1
  50. package/dist/lib/services/generic-entity.service.d.ts.map +1 -1
  51. package/dist/lib/services/generic-entity.service.js +2 -0
  52. package/dist/lib/teams/actions.d.ts.map +1 -1
  53. package/dist/lib/teams/actions.js +0 -1
  54. package/dist/messages/de/billing.json +12 -1
  55. package/dist/messages/de/common.json +2 -0
  56. package/dist/messages/de/docs.json +1 -0
  57. package/dist/messages/de/features.json +16 -4
  58. package/dist/messages/de/home.json +5 -0
  59. package/dist/messages/de/index.d.ts +68 -25
  60. package/dist/messages/de/index.d.ts.map +1 -1
  61. package/dist/messages/de/index.js +2 -0
  62. package/dist/messages/de/index.ts +2 -0
  63. package/dist/messages/de/settings.json +1 -0
  64. package/dist/messages/de/teams.json +3 -18
  65. package/dist/messages/en/billing.json +1 -1
  66. package/dist/messages/en/common.json +2 -0
  67. package/dist/messages/en/docs.json +1 -0
  68. package/dist/messages/en/features.json +16 -4
  69. package/dist/messages/en/home.json +5 -0
  70. package/dist/messages/en/index.d.ts +58 -25
  71. package/dist/messages/en/index.d.ts.map +1 -1
  72. package/dist/messages/en/index.js +2 -0
  73. package/dist/messages/en/index.ts +2 -0
  74. package/dist/messages/en/settings.json +1 -0
  75. package/dist/messages/en/teams.json +3 -18
  76. package/dist/messages/es/billing.json +1 -1
  77. package/dist/messages/es/common.json +2 -0
  78. package/dist/messages/es/docs.json +1 -0
  79. package/dist/messages/es/features.json +16 -4
  80. package/dist/messages/es/home.json +5 -0
  81. package/dist/messages/es/index.d.ts +41 -8
  82. package/dist/messages/es/index.d.ts.map +1 -1
  83. package/dist/messages/es/index.js +2 -0
  84. package/dist/messages/es/index.ts +2 -0
  85. package/dist/messages/es/settings.json +1 -0
  86. package/dist/messages/es/teams.json +3 -0
  87. package/dist/messages/fr/billing.json +12 -1
  88. package/dist/messages/fr/common.json +2 -0
  89. package/dist/messages/fr/docs.json +1 -0
  90. package/dist/messages/fr/features.json +16 -4
  91. package/dist/messages/fr/home.json +5 -0
  92. package/dist/messages/fr/index.d.ts +68 -25
  93. package/dist/messages/fr/index.d.ts.map +1 -1
  94. package/dist/messages/fr/index.js +2 -0
  95. package/dist/messages/fr/index.ts +2 -0
  96. package/dist/messages/fr/settings.json +1 -0
  97. package/dist/messages/fr/teams.json +3 -18
  98. package/dist/messages/it/billing.json +12 -1
  99. package/dist/messages/it/common.json +2 -0
  100. package/dist/messages/it/docs.json +1 -0
  101. package/dist/messages/it/features.json +16 -4
  102. package/dist/messages/it/home.json +5 -0
  103. package/dist/messages/it/index.d.ts +68 -25
  104. package/dist/messages/it/index.d.ts.map +1 -1
  105. package/dist/messages/it/index.js +2 -0
  106. package/dist/messages/it/index.ts +2 -0
  107. package/dist/messages/it/settings.json +1 -0
  108. package/dist/messages/it/teams.json +3 -18
  109. package/dist/messages/pt/billing.json +12 -1
  110. package/dist/messages/pt/common.json +2 -0
  111. package/dist/messages/pt/docs.json +1 -0
  112. package/dist/messages/pt/features.json +16 -4
  113. package/dist/messages/pt/home.json +5 -0
  114. package/dist/messages/pt/index.d.ts +68 -25
  115. package/dist/messages/pt/index.d.ts.map +1 -1
  116. package/dist/messages/pt/index.js +2 -0
  117. package/dist/messages/pt/index.ts +2 -0
  118. package/dist/messages/pt/settings.json +1 -0
  119. package/dist/messages/pt/teams.json +3 -18
  120. package/dist/migrations/029_sample_accounts_outside_development.sql +55 -0
  121. package/dist/migrations/090_sample_data.sql +10 -0
  122. package/dist/routes/_internal/entity-permission-layout.d.ts.map +1 -1
  123. package/dist/routes/_internal/entity-permission-layout.js +0 -7
  124. package/dist/routes/api/mcp/route.d.ts.map +1 -1
  125. package/dist/routes/api/mcp/route.js +7 -1
  126. package/dist/routes/api/v1/media/upload/route.d.ts +2 -1
  127. package/dist/routes/api/v1/media/upload/route.d.ts.map +1 -1
  128. package/dist/routes/api/v1/media/upload/route.js +10 -2
  129. package/dist/routes/api/v1/teams/[teamId]/members/route.d.ts.map +1 -1
  130. package/dist/routes/api/v1/teams/[teamId]/members/route.js +12 -10
  131. package/dist/routes/api/v1/teams/switch/route.d.ts.map +1 -1
  132. package/dist/routes/api/v1/teams/switch/route.js +4 -1
  133. package/dist/routes/dashboard/settings/password/page.d.ts.map +1 -1
  134. package/dist/routes/dashboard/settings/password/page.js +10 -12
  135. package/dist/routes/devtools/config/page.js +1 -1
  136. package/dist/routes/devtools/page.js +1 -1
  137. package/dist/routes/devtools/scheduled-actions/page.js +1 -1
  138. package/dist/routes/devtools/tests/[[...path]]/page.js +1 -1
  139. package/dist/templates/ci-workflows/github/cypress-regression.yml +1 -1
  140. package/dist/templates/ci-workflows/github/cypress-smoke.yml +2 -1
  141. package/dist/templates/projects/starter/README.md +2 -0
  142. package/dist/templates/projects/starter/docs/public/01-overview/01-introduction.md +2 -0
  143. package/dist/templates/projects/starter/templates/dashboard/analytics/layout.tsx +14 -0
  144. package/dist/templates/proxy.ts +15 -8
  145. package/dist/templates/public/uploads/README.md +5 -94
  146. package/migrations/029_sample_accounts_outside_development.sql +55 -0
  147. package/migrations/090_sample_data.sql +10 -0
  148. package/package.json +2 -2
  149. package/scripts/build/registry/host/__tests__/host-proxy-areas.test.mjs +72 -1
  150. package/scripts/build/registry/host/proxy-areas.mjs +65 -2
  151. package/scripts/db/run-migrations.mjs +30 -10
  152. package/scripts/db/sample-data.mjs +44 -0
  153. package/scripts/db/verify-theme-migrations.mjs +6 -0
  154. package/templates/ci-workflows/github/cypress-regression.yml +1 -1
  155. package/templates/ci-workflows/github/cypress-smoke.yml +2 -1
  156. package/templates/projects/starter/README.md +2 -0
  157. package/templates/projects/starter/docs/public/01-overview/01-introduction.md +2 -0
  158. package/templates/projects/starter/templates/dashboard/analytics/layout.tsx +14 -0
  159. package/templates/proxy.ts +15 -8
  160. package/templates/public/uploads/README.md +5 -94
  161. package/tests/jest/__mocks__/@nextsparkjs/registries/api-presets-registry.ts +2 -0
  162. package/tests/jest/__mocks__/@nextsparkjs/registries/mcp-registry.ts +2 -0
  163. package/dist/templates/projects/starter/public/brand/.gitkeep +0 -0
  164. package/dist/templates/public/uploads/.gitignore +0 -6
  165. package/dist/templates/public/uploads/temp/.gitkeep +0 -1
  166. package/templates/projects/starter/public/brand/.gitkeep +0 -0
  167. package/templates/public/uploads/.gitignore +0 -6
  168. package/templates/public/uploads/temp/.gitkeep +0 -1
@@ -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,33 @@ 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 } }), " +
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
+
49
112
  /** The warnings for the proxy Next would load in `projectRoot` (the first of PROXY_FILES that exists). */
50
113
  export function proxyAreaNotices(projectRoot) {
51
114
  if (!projectRoot) return []
52
115
  const file = PROXY_FILES.find(candidate => existsSync(join(projectRoot, candidate)))
53
116
  if (!file) return []
54
- const notice = proxyAreaNotice(file, readFileSync(join(projectRoot, file), 'utf8'))
55
- return notice ? [notice] : []
117
+ const source = readFileSync(join(projectRoot, file), 'utf8')
118
+ return [proxyAreaNotice(file, source), proxySessionNotice(file, source)].filter(Boolean)
56
119
  }
@@ -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);
@@ -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
@@ -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
@@ -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
  ```
@@ -0,0 +1,14 @@
1
+ import { NextIntlClientProvider } from 'next-intl'
2
+ import { getMessages } from 'next-intl/server'
3
+ import { selectMessages } from '@nextsparkjs/core/lib/i18n/client-messages'
4
+
5
+ // TasksChart and RecentActivity are client components that read the `analytics`
6
+ // namespace, which the dashboard group does not carry to the browser by default.
7
+ export default async function AnalyticsLayout({ children }: { children: React.ReactNode }) {
8
+ const messages = await getMessages()
9
+ return (
10
+ <NextIntlClientProvider messages={selectMessages(messages, 'dashboard', ['analytics'])}>
11
+ {children}
12
+ </NextIntlClientProvider>
13
+ )
14
+ }
@@ -20,8 +20,8 @@
20
20
  * themselves and use only x-pathname from here, as a routing hint; the
21
21
  * identity headers are still stripped and re-set for project code.
22
22
  */
23
- import { betterFetch } from '@better-fetch/fetch'
24
23
  import { NextRequest, NextResponse } from 'next/server'
24
+ import { auth } from '@nextsparkjs/core/lib/auth'
25
25
  import {
26
26
  hasProjectMiddleware,
27
27
  executeProjectMiddleware,
@@ -181,14 +181,22 @@ function redirectAccessDenied(request: NextRequest): NextResponse {
181
181
  }
182
182
 
183
183
  /**
184
- * The session the request's cookies carry, asked of this app's own auth route,
185
- * which is served under the base path like every other route.
184
+ * The session the request's cookies carry, read in this process the way core's
185
+ * server code reads it. Asking the app's own /api/auth/get-session over HTTP
186
+ * built the URL from the request's origin, which behind a proxy that
187
+ * terminates TLS (X-Forwarded-Proto: https) is https on a port that speaks
188
+ * plain HTTP: the fetch failed and every signed-in user was sent to /login.
189
+ * Nothing here leaves the process, so no forwarded scheme or host can point
190
+ * the check anywhere. `disableRefresh`: this response cannot carry the renewed
191
+ * session cookie, so the rolling renewal is left to the auth route
192
+ * (lib/auth/session-refresh).
186
193
  */
187
- function getSession(request: NextRequest) {
188
- return betterFetch<Session>('/api/auth/get-session', {
189
- baseURL: `${request.nextUrl.origin}${request.nextUrl.basePath}`,
190
- headers: { cookie: request.headers.get('cookie') || '' },
194
+ async function getSession(request: NextRequest): Promise<Session | null> {
195
+ const session = await auth.api.getSession({
196
+ headers: new Headers({ cookie: request.headers.get('cookie') || '' }),
197
+ query: { disableRefresh: true },
191
198
  })
199
+ return session as Session | null
192
200
  }
193
201
 
194
202
  /**
@@ -490,7 +498,6 @@ export async function proxy(request: NextRequest) {
490
498
  const verifiedSession = async (): Promise<Session | null> => {
491
499
  if (!sessionPromise) {
492
500
  sessionPromise = getSession(request)
493
- .then(({ data }) => data)
494
501
  .catch(error => {
495
502
  sessionError = error
496
503
  return null
@@ -1,97 +1,8 @@
1
- # Media Upload System
1
+ # Media uploads
2
2
 
3
- ## Overview
4
- This application uses **Vercel Blob Storage** for all media uploads. Files are stored in the cloud, not on the local filesystem.
3
+ `POST /api/v1/media/upload` stores files in **Vercel Blob** when `BLOB_READ_WRITE_TOKEN` is set. The media library lists the Blob URLs.
5
4
 
6
- > **Note:** This directory (`public/uploads/`) is legacy from a previous filesystem-based implementation and is no longer used.
5
+ - **Development without a token:** files are written to `public/uploads/temp/` (git-ignored) so the library works offline.
6
+ - **Production without a token:** the upload answers `503` with code `STORAGE_NOT_CONFIGURED`. Nothing is written to disk: a file under `public/` is not served until the next restart and does not survive a redeploy.
7
7
 
8
- ## Upload Endpoint
9
- **POST** `/api/v1/media/upload`
10
-
11
- ### Authentication
12
- Requires valid user session (Better Auth)
13
-
14
- ### Request
15
- - **Content-Type**: `multipart/form-data`
16
- - **Field name**: `files` (accepts multiple files)
17
-
18
- ### Supported File Types
19
- - Images: JPEG, PNG, GIF, WebP
20
- - Videos: MP4, MPEG, QuickTime, WebM
21
-
22
- ### File Size Limit
23
- - Maximum: 10MB per file
24
-
25
- ### Response
26
- ```json
27
- {
28
- "message": "Files uploaded successfully",
29
- "urls": [
30
- "https://xxxxx.public.blob.vercel-storage.com/uploads/temp/1234567890_abc123.jpg",
31
- "https://xxxxx.public.blob.vercel-storage.com/uploads/temp/1234567890_def456.png"
32
- ],
33
- "count": 2
34
- }
35
- ```
36
-
37
- ## File Naming Convention
38
- Files are automatically renamed using the pattern:
39
- ```
40
- {timestamp}_{randomString}.{extension}
41
- ```
42
-
43
- Example: `1704067200000_k9x2m4p.jpg`
44
-
45
- ## Storage Location
46
- - **Storage Provider**: Vercel Blob (cloud object storage)
47
- - **Blob Path**: `uploads/temp/{filename}` (organizational prefix, not a filesystem path)
48
- - **Public URL**: `https://xxxxx.public.blob.vercel-storage.com/uploads/temp/{filename}`
49
-
50
- ### How It Works
51
-
52
- **Local Development:**
53
- - Files are uploaded directly to Vercel Blob using `BLOB_READ_WRITE_TOKEN` from `.env`
54
- - No files are saved to your local filesystem
55
- - URLs point to Vercel's CDN
56
-
57
- **Production/Staging:**
58
- - Same behavior - files go directly to Vercel Blob
59
- - Uses environment-specific Blob token
60
- - No filesystem writes (Vercel serverless is read-only)
61
-
62
- ## Usage in AI Content Generation
63
- The generate-content endpoint (`/api/content-buddy/generate-content`) automatically processes images for AI analysis.
64
-
65
- **Supported URL formats:**
66
- - Vercel Blob URLs: `https://xxxxx.public.blob.vercel-storage.com/uploads/temp/image.jpg`
67
- - External URLs: `https://example.com/image.jpg` (also supported)
68
-
69
- ## Configuration
70
- Requires `BLOB_READ_WRITE_TOKEN` environment variable:
71
-
72
- ```env
73
- BLOB_READ_WRITE_TOKEN="vercel_blob_rw_..."
74
- ```
75
-
76
- ## Cleanup
77
- Files are managed through Vercel Blob's retention policies.
78
-
79
- **Manual cleanup** (if needed):
80
- ```typescript
81
- import { del } from '@vercel/blob'
82
-
83
- // Delete specific file
84
- await del(fileUrl)
85
-
86
- // Or list and delete old files
87
- import { list } from '@vercel/blob'
88
- const { blobs } = await list({ prefix: 'uploads/temp/' })
89
- // Filter and delete as needed
90
- ```
91
-
92
- ## Security Notes
93
- - All uploads require authentication
94
- - File type validation prevents malicious uploads
95
- - File size limits prevent DoS attacks
96
- - Random filenames prevent enumeration attacks
97
- - Files stored in secure Vercel Blob infrastructure
8
+ Self-hosting needs a storage provider: set `BLOB_READ_WRITE_TOKEN` (a Vercel Blob token, starts with `vercel_blob_`). See the media library docs.
@@ -0,0 +1,2 @@
1
+ /** Jest mock: no API presets. */
2
+ export const API_PRESETS_REGISTRY = { endpoints: {} }
@@ -0,0 +1,2 @@
1
+ /** Jest mock: no MCP overrides. */
2
+ export const MCP_OVERRIDES = {}
@@ -1,6 +0,0 @@
1
- # Ignore all uploaded files
2
- temp/*
3
- !temp/.gitkeep
4
-
5
- # Keep the directory structure
6
- !.gitignore
@@ -1 +0,0 @@
1
- # Keep this directory in git but ignore uploaded files
File without changes
@@ -1,6 +0,0 @@
1
- # Ignore all uploaded files
2
- temp/*
3
- !temp/.gitkeep
4
-
5
- # Keep the directory structure
6
- !.gitignore
@@ -1 +0,0 @@
1
- # Keep this directory in git but ignore uploaded files