create-lacspace-app 2.14.0 → 2.14.1

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 CHANGED
@@ -20,6 +20,12 @@ npx create-lacspace-app my-app --template saas --fullstack
20
20
 
21
21
  You choose the *kind* of site you're building. It writes a **real Next.js 15 + React 19 + Tailwind v4 app** — not a hello-world, but a genuinely **polished, modern site**: a fluid `clamp()` type scale, tight display headings, a refined light **and** dark palette, glass chrome, soft layered shadows, a smooth logo marquee, animated counters, scroll reveals and a shimmering primary CTA — every page filled in, an SEO stack wired end-to-end, and a **26-component UI kit** you can drop in anywhere.
22
22
 
23
+ > **Fixed in v2.14.1 — login, and a hardened API.**
24
+ > - **Email is normalised before it is queried.** Mongoose applies `lowercase`/`trim` when it *saves*, but not to query filters — so an account registered as `Bro@Gmail.com` was stored as `bro@gmail.com` and then could not be found at login. Full-stack projects generated before this should apply the one-line change in `backend/src/validation.ts`.
25
+ > - **The API now sends the same class of security headers as the frontend** (`@lacspace/headers`: CSP, `X-Frame-Options: DENY`, `no-referrer`, nosniff, CORP), stops advertising `X-Powered-By: Express`, and caps JSON bodies at 256kb.
26
+ > - **`npm run typecheck` covers both workspaces.** It only ran the backend, so it reported green over an unchecked frontend.
27
+ > - **Every template ships a skip-to-content link** (WCAG 2.4.1) — hidden until focused, and focus really moves rather than only scrolling.
28
+
23
29
  > **New in v2.14 — the UI kit (18 add-ons).** The scaffold now comes with a real component layer, not just a page:
24
30
  > - **`ui`** — the **96-component `@lacspace/components` library** wired into your template: a working settings page (form validation, a confirm dialog that waits for its promise, toasts), a `<UIProvider>` for app-wide use, and a **theme bridge** that maps your template's accent, radius and font onto the kit's `--lac-*` variables — so the components match your brand and follow your existing dark-mode toggle out of the box. Frontend, any template.
25
31
  > - **`dataviz`** — a working `/insights` page from **`@lacspace/charts` + `@lacspace/table` + `@lacspace/date`**: stat cards, an SVG line chart, a date-range filter and a sortable, searchable, CSV-exportable table. No canvas, no D3, no grid licence. Frontend, any template.
package/dist/index.js CHANGED
@@ -61,7 +61,8 @@ var pkgJson = (ctx) => JSON.stringify({
61
61
  name: ctx.name,
62
62
  version: "0.1.0",
63
63
  private: true,
64
- scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint" },
64
+ // `typecheck` so the full-stack root script can run it across both workspaces.
65
+ scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint", typecheck: "tsc --noEmit" },
65
66
  dependencies: {
66
67
  next: "^15.1.0",
67
68
  react: "^19.0.0",
@@ -239,6 +240,27 @@ h1, .display { letter-spacing: -0.035em; line-height: 1.02; }
239
240
  ::-webkit-scrollbar-thumb:hover { background: var(--hairline); }
240
241
  :focus-visible { outline: 2px solid var(--accent-to); outline-offset: 2px; border-radius: 6px; }
241
242
 
243
+ /* Skip link: off-screen until it is focused, then the first thing you see.
244
+ The wrapper it targets carries tabIndex={-1}, so focus actually moves there
245
+ instead of the browser only scrolling. */
246
+ .skip-link {
247
+ position: fixed;
248
+ top: 10px;
249
+ left: 10px;
250
+ z-index: 100;
251
+ padding: 10px 16px;
252
+ border-radius: 10px;
253
+ background: var(--accent-to);
254
+ color: var(--on-accent);
255
+ font-weight: 600;
256
+ text-decoration: none;
257
+ transform: translateY(-160%);
258
+ transition: transform 0.18s ease;
259
+ }
260
+ .skip-link:focus { transform: translateY(0); }
261
+ @media (prefers-reduced-motion: reduce) { .skip-link { transition: none; } }
262
+ #main:focus { outline: none; }
263
+
242
264
  .gradient-text {
243
265
  background: linear-gradient(115deg, var(--accent-from), var(--accent-to));
244
266
  -webkit-background-clip: text; background-clip: text; color: transparent;
@@ -403,11 +425,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
403
425
  return (
404
426
  <html lang="en" className={\`\${inter.variable} \${inter.className}\`} suppressHydrationWarning>
405
427
  <body className="antialiased">
428
+ {/* Keyboard users land here first and can jump past the nav (WCAG 2.4.1). */}
429
+ <a href="#main" className="skip-link">Skip to content</a>
406
430
  {/* \u2728 Dark / light / system theming with a built-in no-flash script \u2014 @lacspace/theme */}
407
431
  <ThemeProvider defaultTheme="dark">
408
432
  {/* \u2728 Press \u2318K / Ctrl-K anywhere \u2014 powered by @lacspace/ui */}
409
433
  <CommandMenu />
410
- ${open}{children}${close}
434
+ ${open}<div id="main" tabIndex={-1}>{children}</div>${close}
411
435
  </ThemeProvider>
412
436
  <script
413
437
  type="application/ld+json"
@@ -7440,7 +7464,9 @@ var rootPkgJson = (ctx) => JSON.stringify({
7440
7464
  start: 'concurrently -k -n api,web -c blue,magenta "npm:start:api" "npm:start:web"',
7441
7465
  "start:api": `npm run start --workspace ${scope(ctx)}/backend`,
7442
7466
  "start:web": `npm run start --workspace ${scope(ctx)}/frontend`,
7443
- typecheck: `npm run typecheck --workspace ${scope(ctx)}/backend`
7467
+ // Both workspaces: a gate that silently skips the frontend is worse than
7468
+ // no gate, because it reports green over unchecked code.
7469
+ typecheck: `npm run typecheck --workspace ${scope(ctx)}/backend && npm run typecheck --workspace ${scope(ctx)}/frontend`
7444
7470
  },
7445
7471
  devDependencies: { concurrently: "^9.1.0" },
7446
7472
  engines: { node: ">=18" }
@@ -7825,6 +7851,7 @@ var backendPkgJson = (ctx) => {
7825
7851
  "@lacspace/rate-limit": "^1.2.0",
7826
7852
  "@lacspace/cache": "^1.1.0",
7827
7853
  "@lacspace/logger": "^1.0.0",
7854
+ "@lacspace/headers": "^1.1.2",
7828
7855
  ...featureDeps,
7829
7856
  [`${scope(ctx)}/types`]: "*"
7830
7857
  },
@@ -8153,14 +8180,18 @@ var backendValidation = () => `import { v, type Infer } from "@lacspace/validate
8153
8180
  // how this works: Zod-like schemas (@lacspace/validate). .parse(body) throws a
8154
8181
  // ValidationError on bad input, which the error middleware turns into a clean 400
8155
8182
  // with per-field messages.
8183
+ // Email is normalised HERE, not only in the schema. Mongoose applies
8184
+ // \`lowercase\`/\`trim\` setters when it SAVES a document, but not to query
8185
+ // filters \u2014 so \`User.findOne({ email })\` with "Bro@Gmail.com" would miss the
8186
+ // stored "bro@gmail.com", and the account would be unreachable at login.
8156
8187
  export const RegisterInput = v.object({
8157
8188
  name: v.string().min(2).max(80),
8158
- email: v.string().email(),
8189
+ email: v.string().email().trim().toLowerCase(),
8159
8190
  password: v.string().min(8).max(200),
8160
8191
  });
8161
8192
 
8162
8193
  export const LoginInput = v.object({
8163
- email: v.string().email(),
8194
+ email: v.string().email().trim().toLowerCase(),
8164
8195
  password: v.string().min(1),
8165
8196
  });
8166
8197
 
@@ -8296,19 +8327,35 @@ export default router;
8296
8327
  `;
8297
8328
  var backendApp = (ctx) => `import express from "express";
8298
8329
  import cors from "cors";
8330
+ import { expressSecurityHeaders } from "@lacspace/headers";
8299
8331
  import { env } from "./env.js";
8300
8332
  import { requestLogger } from "./logger.js";
8301
8333
  import { errorHandler } from "./middleware/error.js";
8302
8334
  import { registerRoutes } from "./routes/index.js";
8303
8335
 
8304
- // how this works: assembles the Express app \u2014 CORS for the frontend, JSON parsing,
8305
- // structured request logging (@lacspace/logger), a health check, all route groups
8306
- // (see routes/index.ts), then the error handler LAST.
8336
+ // how this works: assembles the Express app \u2014 security headers, CORS for the
8337
+ // frontend, JSON parsing, structured request logging (@lacspace/logger), a health
8338
+ // check, all route groups (see routes/index.ts), then the error handler LAST.
8307
8339
  export function createApp(): express.Express {
8308
8340
  const app = express();
8309
8341
 
8342
+ // Express advertises itself in every response by default; there is no reason
8343
+ // to tell the internet what the API is built on.
8344
+ app.disable("x-powered-by");
8345
+
8346
+ // The same hardening the frontend gets, tuned for an API: deny framing, send
8347
+ // no referrer, and a CSP that allows nothing \u2014 an API returns JSON, so there
8348
+ // is no script, style or image for a browser to load from it.
8349
+ app.use(expressSecurityHeaders({
8350
+ frameOptions: "DENY",
8351
+ referrerPolicy: "no-referrer",
8352
+ crossOriginResourcePolicy: "same-site",
8353
+ contentSecurityPolicy: { defaultSrc: ["'none'"], frameAncestors: ["'none'"] },
8354
+ }));
8355
+
8310
8356
  app.use(cors({ origin: env.CORS_ORIGIN.split(",").map((o) => o.trim()), credentials: true }));
8311
- app.use(express.json());
8357
+ // A JSON API has no business accepting a megabyte of body by default.
8358
+ app.use(express.json({ limit: "256kb" }));
8312
8359
  app.use(requestLogger); // one structured log line per request
8313
8360
 
8314
8361
  app.get("/health", (_req, res) => { res.json({ ok: true, service: "${ctx.name}-api" }); });
package/dist/lib.cjs CHANGED
@@ -65,7 +65,8 @@ var pkgJson = (ctx) => JSON.stringify({
65
65
  name: ctx.name,
66
66
  version: "0.1.0",
67
67
  private: true,
68
- scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint" },
68
+ // `typecheck` so the full-stack root script can run it across both workspaces.
69
+ scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint", typecheck: "tsc --noEmit" },
69
70
  dependencies: {
70
71
  next: "^15.1.0",
71
72
  react: "^19.0.0",
@@ -243,6 +244,27 @@ h1, .display { letter-spacing: -0.035em; line-height: 1.02; }
243
244
  ::-webkit-scrollbar-thumb:hover { background: var(--hairline); }
244
245
  :focus-visible { outline: 2px solid var(--accent-to); outline-offset: 2px; border-radius: 6px; }
245
246
 
247
+ /* Skip link: off-screen until it is focused, then the first thing you see.
248
+ The wrapper it targets carries tabIndex={-1}, so focus actually moves there
249
+ instead of the browser only scrolling. */
250
+ .skip-link {
251
+ position: fixed;
252
+ top: 10px;
253
+ left: 10px;
254
+ z-index: 100;
255
+ padding: 10px 16px;
256
+ border-radius: 10px;
257
+ background: var(--accent-to);
258
+ color: var(--on-accent);
259
+ font-weight: 600;
260
+ text-decoration: none;
261
+ transform: translateY(-160%);
262
+ transition: transform 0.18s ease;
263
+ }
264
+ .skip-link:focus { transform: translateY(0); }
265
+ @media (prefers-reduced-motion: reduce) { .skip-link { transition: none; } }
266
+ #main:focus { outline: none; }
267
+
246
268
  .gradient-text {
247
269
  background: linear-gradient(115deg, var(--accent-from), var(--accent-to));
248
270
  -webkit-background-clip: text; background-clip: text; color: transparent;
@@ -407,11 +429,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
407
429
  return (
408
430
  <html lang="en" className={\`\${inter.variable} \${inter.className}\`} suppressHydrationWarning>
409
431
  <body className="antialiased">
432
+ {/* Keyboard users land here first and can jump past the nav (WCAG 2.4.1). */}
433
+ <a href="#main" className="skip-link">Skip to content</a>
410
434
  {/* \u2728 Dark / light / system theming with a built-in no-flash script \u2014 @lacspace/theme */}
411
435
  <ThemeProvider defaultTheme="dark">
412
436
  {/* \u2728 Press \u2318K / Ctrl-K anywhere \u2014 powered by @lacspace/ui */}
413
437
  <CommandMenu />
414
- ${open}{children}${close}
438
+ ${open}<div id="main" tabIndex={-1}>{children}</div>${close}
415
439
  </ThemeProvider>
416
440
  <script
417
441
  type="application/ld+json"
@@ -7444,7 +7468,9 @@ var rootPkgJson = (ctx) => JSON.stringify({
7444
7468
  start: 'concurrently -k -n api,web -c blue,magenta "npm:start:api" "npm:start:web"',
7445
7469
  "start:api": `npm run start --workspace ${scope(ctx)}/backend`,
7446
7470
  "start:web": `npm run start --workspace ${scope(ctx)}/frontend`,
7447
- typecheck: `npm run typecheck --workspace ${scope(ctx)}/backend`
7471
+ // Both workspaces: a gate that silently skips the frontend is worse than
7472
+ // no gate, because it reports green over unchecked code.
7473
+ typecheck: `npm run typecheck --workspace ${scope(ctx)}/backend && npm run typecheck --workspace ${scope(ctx)}/frontend`
7448
7474
  },
7449
7475
  devDependencies: { concurrently: "^9.1.0" },
7450
7476
  engines: { node: ">=18" }
@@ -7829,6 +7855,7 @@ var backendPkgJson = (ctx) => {
7829
7855
  "@lacspace/rate-limit": "^1.2.0",
7830
7856
  "@lacspace/cache": "^1.1.0",
7831
7857
  "@lacspace/logger": "^1.0.0",
7858
+ "@lacspace/headers": "^1.1.2",
7832
7859
  ...featureDeps,
7833
7860
  [`${scope(ctx)}/types`]: "*"
7834
7861
  },
@@ -8157,14 +8184,18 @@ var backendValidation = () => `import { v, type Infer } from "@lacspace/validate
8157
8184
  // how this works: Zod-like schemas (@lacspace/validate). .parse(body) throws a
8158
8185
  // ValidationError on bad input, which the error middleware turns into a clean 400
8159
8186
  // with per-field messages.
8187
+ // Email is normalised HERE, not only in the schema. Mongoose applies
8188
+ // \`lowercase\`/\`trim\` setters when it SAVES a document, but not to query
8189
+ // filters \u2014 so \`User.findOne({ email })\` with "Bro@Gmail.com" would miss the
8190
+ // stored "bro@gmail.com", and the account would be unreachable at login.
8160
8191
  export const RegisterInput = v.object({
8161
8192
  name: v.string().min(2).max(80),
8162
- email: v.string().email(),
8193
+ email: v.string().email().trim().toLowerCase(),
8163
8194
  password: v.string().min(8).max(200),
8164
8195
  });
8165
8196
 
8166
8197
  export const LoginInput = v.object({
8167
- email: v.string().email(),
8198
+ email: v.string().email().trim().toLowerCase(),
8168
8199
  password: v.string().min(1),
8169
8200
  });
8170
8201
 
@@ -8300,19 +8331,35 @@ export default router;
8300
8331
  `;
8301
8332
  var backendApp = (ctx) => `import express from "express";
8302
8333
  import cors from "cors";
8334
+ import { expressSecurityHeaders } from "@lacspace/headers";
8303
8335
  import { env } from "./env.js";
8304
8336
  import { requestLogger } from "./logger.js";
8305
8337
  import { errorHandler } from "./middleware/error.js";
8306
8338
  import { registerRoutes } from "./routes/index.js";
8307
8339
 
8308
- // how this works: assembles the Express app \u2014 CORS for the frontend, JSON parsing,
8309
- // structured request logging (@lacspace/logger), a health check, all route groups
8310
- // (see routes/index.ts), then the error handler LAST.
8340
+ // how this works: assembles the Express app \u2014 security headers, CORS for the
8341
+ // frontend, JSON parsing, structured request logging (@lacspace/logger), a health
8342
+ // check, all route groups (see routes/index.ts), then the error handler LAST.
8311
8343
  export function createApp(): express.Express {
8312
8344
  const app = express();
8313
8345
 
8346
+ // Express advertises itself in every response by default; there is no reason
8347
+ // to tell the internet what the API is built on.
8348
+ app.disable("x-powered-by");
8349
+
8350
+ // The same hardening the frontend gets, tuned for an API: deny framing, send
8351
+ // no referrer, and a CSP that allows nothing \u2014 an API returns JSON, so there
8352
+ // is no script, style or image for a browser to load from it.
8353
+ app.use(expressSecurityHeaders({
8354
+ frameOptions: "DENY",
8355
+ referrerPolicy: "no-referrer",
8356
+ crossOriginResourcePolicy: "same-site",
8357
+ contentSecurityPolicy: { defaultSrc: ["'none'"], frameAncestors: ["'none'"] },
8358
+ }));
8359
+
8314
8360
  app.use(cors({ origin: env.CORS_ORIGIN.split(",").map((o) => o.trim()), credentials: true }));
8315
- app.use(express.json());
8361
+ // A JSON API has no business accepting a megabyte of body by default.
8362
+ app.use(express.json({ limit: "256kb" }));
8316
8363
  app.use(requestLogger); // one structured log line per request
8317
8364
 
8318
8365
  app.get("/health", (_req, res) => { res.json({ ok: true, service: "${ctx.name}-api" }); });
package/dist/lib.js CHANGED
@@ -62,7 +62,8 @@ var pkgJson = (ctx) => JSON.stringify({
62
62
  name: ctx.name,
63
63
  version: "0.1.0",
64
64
  private: true,
65
- scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint" },
65
+ // `typecheck` so the full-stack root script can run it across both workspaces.
66
+ scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint", typecheck: "tsc --noEmit" },
66
67
  dependencies: {
67
68
  next: "^15.1.0",
68
69
  react: "^19.0.0",
@@ -240,6 +241,27 @@ h1, .display { letter-spacing: -0.035em; line-height: 1.02; }
240
241
  ::-webkit-scrollbar-thumb:hover { background: var(--hairline); }
241
242
  :focus-visible { outline: 2px solid var(--accent-to); outline-offset: 2px; border-radius: 6px; }
242
243
 
244
+ /* Skip link: off-screen until it is focused, then the first thing you see.
245
+ The wrapper it targets carries tabIndex={-1}, so focus actually moves there
246
+ instead of the browser only scrolling. */
247
+ .skip-link {
248
+ position: fixed;
249
+ top: 10px;
250
+ left: 10px;
251
+ z-index: 100;
252
+ padding: 10px 16px;
253
+ border-radius: 10px;
254
+ background: var(--accent-to);
255
+ color: var(--on-accent);
256
+ font-weight: 600;
257
+ text-decoration: none;
258
+ transform: translateY(-160%);
259
+ transition: transform 0.18s ease;
260
+ }
261
+ .skip-link:focus { transform: translateY(0); }
262
+ @media (prefers-reduced-motion: reduce) { .skip-link { transition: none; } }
263
+ #main:focus { outline: none; }
264
+
243
265
  .gradient-text {
244
266
  background: linear-gradient(115deg, var(--accent-from), var(--accent-to));
245
267
  -webkit-background-clip: text; background-clip: text; color: transparent;
@@ -404,11 +426,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
404
426
  return (
405
427
  <html lang="en" className={\`\${inter.variable} \${inter.className}\`} suppressHydrationWarning>
406
428
  <body className="antialiased">
429
+ {/* Keyboard users land here first and can jump past the nav (WCAG 2.4.1). */}
430
+ <a href="#main" className="skip-link">Skip to content</a>
407
431
  {/* \u2728 Dark / light / system theming with a built-in no-flash script \u2014 @lacspace/theme */}
408
432
  <ThemeProvider defaultTheme="dark">
409
433
  {/* \u2728 Press \u2318K / Ctrl-K anywhere \u2014 powered by @lacspace/ui */}
410
434
  <CommandMenu />
411
- ${open}{children}${close}
435
+ ${open}<div id="main" tabIndex={-1}>{children}</div>${close}
412
436
  </ThemeProvider>
413
437
  <script
414
438
  type="application/ld+json"
@@ -7441,7 +7465,9 @@ var rootPkgJson = (ctx) => JSON.stringify({
7441
7465
  start: 'concurrently -k -n api,web -c blue,magenta "npm:start:api" "npm:start:web"',
7442
7466
  "start:api": `npm run start --workspace ${scope(ctx)}/backend`,
7443
7467
  "start:web": `npm run start --workspace ${scope(ctx)}/frontend`,
7444
- typecheck: `npm run typecheck --workspace ${scope(ctx)}/backend`
7468
+ // Both workspaces: a gate that silently skips the frontend is worse than
7469
+ // no gate, because it reports green over unchecked code.
7470
+ typecheck: `npm run typecheck --workspace ${scope(ctx)}/backend && npm run typecheck --workspace ${scope(ctx)}/frontend`
7445
7471
  },
7446
7472
  devDependencies: { concurrently: "^9.1.0" },
7447
7473
  engines: { node: ">=18" }
@@ -7826,6 +7852,7 @@ var backendPkgJson = (ctx) => {
7826
7852
  "@lacspace/rate-limit": "^1.2.0",
7827
7853
  "@lacspace/cache": "^1.1.0",
7828
7854
  "@lacspace/logger": "^1.0.0",
7855
+ "@lacspace/headers": "^1.1.2",
7829
7856
  ...featureDeps,
7830
7857
  [`${scope(ctx)}/types`]: "*"
7831
7858
  },
@@ -8154,14 +8181,18 @@ var backendValidation = () => `import { v, type Infer } from "@lacspace/validate
8154
8181
  // how this works: Zod-like schemas (@lacspace/validate). .parse(body) throws a
8155
8182
  // ValidationError on bad input, which the error middleware turns into a clean 400
8156
8183
  // with per-field messages.
8184
+ // Email is normalised HERE, not only in the schema. Mongoose applies
8185
+ // \`lowercase\`/\`trim\` setters when it SAVES a document, but not to query
8186
+ // filters \u2014 so \`User.findOne({ email })\` with "Bro@Gmail.com" would miss the
8187
+ // stored "bro@gmail.com", and the account would be unreachable at login.
8157
8188
  export const RegisterInput = v.object({
8158
8189
  name: v.string().min(2).max(80),
8159
- email: v.string().email(),
8190
+ email: v.string().email().trim().toLowerCase(),
8160
8191
  password: v.string().min(8).max(200),
8161
8192
  });
8162
8193
 
8163
8194
  export const LoginInput = v.object({
8164
- email: v.string().email(),
8195
+ email: v.string().email().trim().toLowerCase(),
8165
8196
  password: v.string().min(1),
8166
8197
  });
8167
8198
 
@@ -8297,19 +8328,35 @@ export default router;
8297
8328
  `;
8298
8329
  var backendApp = (ctx) => `import express from "express";
8299
8330
  import cors from "cors";
8331
+ import { expressSecurityHeaders } from "@lacspace/headers";
8300
8332
  import { env } from "./env.js";
8301
8333
  import { requestLogger } from "./logger.js";
8302
8334
  import { errorHandler } from "./middleware/error.js";
8303
8335
  import { registerRoutes } from "./routes/index.js";
8304
8336
 
8305
- // how this works: assembles the Express app \u2014 CORS for the frontend, JSON parsing,
8306
- // structured request logging (@lacspace/logger), a health check, all route groups
8307
- // (see routes/index.ts), then the error handler LAST.
8337
+ // how this works: assembles the Express app \u2014 security headers, CORS for the
8338
+ // frontend, JSON parsing, structured request logging (@lacspace/logger), a health
8339
+ // check, all route groups (see routes/index.ts), then the error handler LAST.
8308
8340
  export function createApp(): express.Express {
8309
8341
  const app = express();
8310
8342
 
8343
+ // Express advertises itself in every response by default; there is no reason
8344
+ // to tell the internet what the API is built on.
8345
+ app.disable("x-powered-by");
8346
+
8347
+ // The same hardening the frontend gets, tuned for an API: deny framing, send
8348
+ // no referrer, and a CSP that allows nothing \u2014 an API returns JSON, so there
8349
+ // is no script, style or image for a browser to load from it.
8350
+ app.use(expressSecurityHeaders({
8351
+ frameOptions: "DENY",
8352
+ referrerPolicy: "no-referrer",
8353
+ crossOriginResourcePolicy: "same-site",
8354
+ contentSecurityPolicy: { defaultSrc: ["'none'"], frameAncestors: ["'none'"] },
8355
+ }));
8356
+
8311
8357
  app.use(cors({ origin: env.CORS_ORIGIN.split(",").map((o) => o.trim()), credentials: true }));
8312
- app.use(express.json());
8358
+ // A JSON API has no business accepting a megabyte of body by default.
8359
+ app.use(express.json({ limit: "256kb" }));
8313
8360
  app.use(requestLogger); // one structured log line per request
8314
8361
 
8315
8362
  app.get("/health", (_req, res) => { res.json({ ok: true, service: "${ctx.name}-api" }); });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-lacspace-app",
3
- "version": "2.14.0",
4
- "description": "Scaffold a beautiful, production-ready Next.js app from a Lacspace template — portfolio, business, e-commerce, SaaS, blog, docs, dashboard, restaurant or marketplace — pre-wired with SEO, security headers, sitemap and robots. Use it as a CLI (like create-next-app, but you start gorgeous) or import it as a library to generate projects programmatically.",
3
+ "version": "2.14.1",
4
+ "description": "Scaffold a beautiful, production-ready Next.js app from a Lacspace template \u2014 portfolio, business, e-commerce, SaaS, blog, docs, dashboard, restaurant or marketplace \u2014 pre-wired with SEO, security headers, sitemap and robots. Use it as a CLI (like create-next-app, but you start gorgeous) or import it as a library to generate projects programmatically.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-lacspace-app": "dist/index.js"