create-zerotal 1.4.0 → 1.5.0

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/CHANGELOG.md CHANGED
@@ -8,6 +8,26 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.5.0] — 2026-08-15
12
+
13
+ ### Changed
14
+
15
+ - **Scaffolded apps no longer default CORS to `"*"`.** Every template shipped
16
+ `cors: { origin: env("CORS_ORIGIN", "*") }`, so a new app opted out of the
17
+ framework's own safe default — an empty list, meaning same-origin — without ever
18
+ being asked. `"*"` lets any website read the app's responses out of a visitor's
19
+ browser, and it survived to production in anything that never set `CORS_ORIGIN`.
20
+ The default is now same-origin; name your origins when you need them.
21
+ - **Templates declare `secureHeaders.secure`**, the setting that gates HSTS, so
22
+ turning it on for a deployment is a visible one-line change rather than a
23
+ discovery. `zt doctor` fails on a production-like deployment that leaves it off.
24
+
25
+ - **The `api` template registers `StorageProvider`.** Its absence is invisible until the
26
+ first upload: a `Storage.disk(...)` call usually sits behind auth, a permission check
27
+ and a multipart parse, so every simpler probe returns 401/403 long before reaching the
28
+ line that would fail — and that line is often first reached in production. It costs
29
+ nothing when unused. The `flow`, `react`, `vue` and `admin` templates already had it.
30
+
11
31
  ## [1.3.0] — 2026-08-09
12
32
 
13
33
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zerotal",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Create a new Zerotal application",
5
5
  "license": "MIT",
6
6
  "maturity": "stable",
package/src/scaffold.ts CHANGED
@@ -13,7 +13,7 @@ export type Template = 'minimal' | 'api' | 'admin' | 'flow' | 'react' | 'vue';
13
13
  // "^1.1.0" found for specifier "zerotal"` — the first thing anyone trying the
14
14
  // framework saw. `scaffold.test.ts` now asserts the two agree, so CI fails rather
15
15
  // than the user's install.
16
- export const ZT_VERSION = '^1.4.0';
16
+ export const ZT_VERSION = '^1.5.0';
17
17
 
18
18
  export interface ScaffoldOptions {
19
19
  name: string;
@@ -10,11 +10,18 @@ export default AppConfig({
10
10
  */
11
11
  key: requireEnv("APP_KEY"),
12
12
 
13
+ // Same-origin unless CORS_ORIGIN names somewhere else. `"*"` would let any
14
+ // website read this app's responses out of a visitor's browser.
13
15
  cors: {
14
- origin: env("CORS_ORIGIN", "*"),
16
+ origin: env("CORS_ORIGIN", ""),
15
17
  credentials: false,
16
18
  },
17
19
 
20
+ // HSTS, once you are serving over HTTPS.
21
+ secureHeaders: {
22
+ secure: env("APP_SECURE", false),
23
+ },
24
+
18
25
  throttle: {
19
26
  maxAttempts: 120,
20
27
  windowSeconds: 60,
@@ -18,6 +18,7 @@
18
18
  ]
19
19
  }
20
20
  },
21
+ "include": ["**/*"],
21
22
  "exclude": [
22
23
  "node_modules",
23
24
  ".zerotal"
@@ -5,6 +5,7 @@ import { AuthProvider } from 'zerotal/auth';
5
5
  import { NotificationProvider } from '@zerotal/notifications';
6
6
  import { QueueProvider } from 'zerotal/queue';
7
7
  import { LogProvider } from 'zerotal/logger';
8
+ import { StorageProvider } from 'zerotal/storage';
8
9
  import { DevtoolsProvider } from '@zerotal/devtools';
9
10
 
10
11
  // `DevtoolsProvider` needs no configuration. It activates only when APP_ENV is
@@ -27,6 +28,12 @@ export default Application.create({
27
28
  NotificationProvider,
28
29
  QueueProvider,
29
30
  LogProvider,
31
+ // Registered up front because its absence is invisible until the first
32
+ // upload. A `Storage.disk(...)` call usually sits behind auth, a permission
33
+ // check and a multipart parse, so every simpler probe returns 401/403 long
34
+ // before reaching the line that would fail — and the line that would fail
35
+ // is usually first reached in production. It costs nothing when unused.
36
+ StorageProvider,
30
37
  DevtoolsProvider,
31
38
  ],
32
39
  })
@@ -11,6 +11,7 @@
11
11
  "noEmit": true,
12
12
  "allowImportingTsExtensions": true
13
13
  },
14
+ "include": ["**/*"],
14
15
  "exclude": [
15
16
  "node_modules",
16
17
  ".zerotal"
@@ -10,11 +10,18 @@ export default AppConfig({
10
10
  */
11
11
  key: requireEnv("APP_KEY"),
12
12
 
13
+ // Same-origin unless CORS_ORIGIN names somewhere else. `"*"` would let any
14
+ // website read this app's responses out of a visitor's browser.
13
15
  cors: {
14
- origin: env("CORS_ORIGIN", "*"),
16
+ origin: env("CORS_ORIGIN", ""),
15
17
  credentials: false,
16
18
  },
17
19
 
20
+ // HSTS, once you are serving over HTTPS.
21
+ secureHeaders: {
22
+ secure: env("APP_SECURE", false),
23
+ },
24
+
18
25
  throttle: {
19
26
  maxAttempts: 120,
20
27
  windowSeconds: 60,
@@ -18,6 +18,7 @@
18
18
  "noEmit": true,
19
19
  "allowImportingTsExtensions": true
20
20
  },
21
+ "include": ["**/*"],
21
22
  "exclude": [
22
23
  "node_modules",
23
24
  ".zerotal"
@@ -10,11 +10,18 @@ export default AppConfig({
10
10
  */
11
11
  key: requireEnv("APP_KEY"),
12
12
 
13
+ // Same-origin unless CORS_ORIGIN names somewhere else. `"*"` would let any
14
+ // website read this app's responses out of a visitor's browser.
13
15
  cors: {
14
- origin: env("CORS_ORIGIN", "*"),
16
+ origin: env("CORS_ORIGIN", ""),
15
17
  credentials: false,
16
18
  },
17
19
 
20
+ // HSTS, once you are serving over HTTPS.
21
+ secureHeaders: {
22
+ secure: env("APP_SECURE", false),
23
+ },
24
+
18
25
  throttle: {
19
26
  maxAttempts: 120,
20
27
  windowSeconds: 60,
@@ -21,6 +21,7 @@
21
21
  "noEmit": true,
22
22
  "allowImportingTsExtensions": true
23
23
  },
24
+ "include": ["**/*"],
24
25
  "exclude": [
25
26
  "node_modules",
26
27
  ".zerotal"
@@ -10,11 +10,18 @@ export default AppConfig({
10
10
  */
11
11
  key: requireEnv("APP_KEY"),
12
12
 
13
+ // Same-origin unless CORS_ORIGIN names somewhere else. `"*"` would let any
14
+ // website read this app's responses out of a visitor's browser.
13
15
  cors: {
14
- origin: env("CORS_ORIGIN", "*"),
16
+ origin: env("CORS_ORIGIN", ""),
15
17
  credentials: false,
16
18
  },
17
19
 
20
+ // HSTS, once you are serving over HTTPS.
21
+ secureHeaders: {
22
+ secure: env("APP_SECURE", false),
23
+ },
24
+
18
25
  throttle: {
19
26
  maxAttempts: 120,
20
27
  windowSeconds: 60,
@@ -11,7 +11,10 @@ createInertiaApp({
11
11
  // Map a component name (e.g. "Users/Index") to its lazily-loaded module.
12
12
  // `pages` is generated from the configured pages dir by `inertia:build`.
13
13
  resolve: async (name): Promise<ResolvedComponent> => {
14
- const page = pages[name];
14
+ // `pages` is keyed by literal page name (written with `satisfies`, so the
15
+ // server can check `Inertia.render("Users/Index", …)` against it). The name
16
+ // Inertia hands us is a runtime string, so this one lookup widens it.
17
+ const page = (pages as Record<string, () => Promise<{ default: unknown }>>)[name];
15
18
  if (!page) throw new Error(`Inertia page not found: "${name}"`);
16
19
  return (await page()).default as ResolvedComponent;
17
20
  },
@@ -5,7 +5,7 @@
5
5
  // Bun.build with splitting:true creates one .js chunk per page.
6
6
  // Converting to static imports will bundle ALL pages into app.js.
7
7
 
8
- export const pages: Record<string, () => Promise<{ default: unknown }>> = {
8
+ export const pages = {
9
9
  about: () => import("./pages/about.tsx"),
10
10
  contact: () => import("./pages/contact.tsx"),
11
11
  error: () => import("./pages/error.tsx"),
@@ -15,4 +15,13 @@ export const pages: Record<string, () => Promise<{ default: unknown }>> = {
15
15
  profile: () => import("./pages/profile.tsx"),
16
16
  register: () => import("./pages/register.tsx"),
17
17
  "reset-password": () => import("./pages/reset-password.tsx"),
18
- };
18
+ } satisfies Record<string, () => Promise<{ default: unknown }>>;
19
+
20
+ // Type-only: lets a controller's `Inertia.render(name, props)` be checked
21
+ // against the props each page component declares. Erased at runtime — and
22
+ // it is the ONLY place the server side touches the client component graph.
23
+ declare module "@zerotal/inertia" {
24
+ interface InertiaPageRegistry {
25
+ pages: typeof pages;
26
+ }
27
+ }
@@ -15,3 +15,18 @@ export interface SharedProps {
15
15
  old: Record<string, unknown>;
16
16
  [key: string]: unknown;
17
17
  }
18
+
19
+ /**
20
+ * The same list, told to the server side. `Inertia.render(page, props)` checks a
21
+ * controller's props against the page component's own props — so without this,
22
+ * a page that reads `auth` through `usePage()` would look to the server like a
23
+ * page whose controller forgot to pass it.
24
+ *
25
+ * `share()` is a runtime call in a provider, so nothing can generate this: when
26
+ * you register a new shared prop, add it in both places.
27
+ */
28
+ declare module "@zerotal/inertia" {
29
+ interface SharedProps {
30
+ auth: { user: { id: number; name: string; email: string } | null };
31
+ }
32
+ }
@@ -21,6 +21,7 @@
21
21
  ]
22
22
  }
23
23
  },
24
+ "include": ["**/*"],
24
25
  "exclude": [
25
26
  "node_modules",
26
27
  ".zerotal"
@@ -10,11 +10,18 @@ export default AppConfig({
10
10
  */
11
11
  key: requireEnv("APP_KEY"),
12
12
 
13
+ // Same-origin unless CORS_ORIGIN names somewhere else. `"*"` would let any
14
+ // website read this app's responses out of a visitor's browser.
13
15
  cors: {
14
- origin: env("CORS_ORIGIN", "*"),
16
+ origin: env("CORS_ORIGIN", ""),
15
17
  credentials: false,
16
18
  },
17
19
 
20
+ // HSTS, once you are serving over HTTPS.
21
+ secureHeaders: {
22
+ secure: env("APP_SECURE", false),
23
+ },
24
+
18
25
  throttle: {
19
26
  maxAttempts: 120,
20
27
  windowSeconds: 60,
@@ -7,7 +7,10 @@ createInertiaApp({
7
7
  // Map a component name (e.g. "Users/Index") to its lazily-loaded module.
8
8
  // `pages` is generated from the configured pages dir by `inertia:build`.
9
9
  resolve: async (name): Promise<DefineComponent> => {
10
- const page = pages[name];
10
+ // `pages` is keyed by literal page name (written with `satisfies`, so the
11
+ // server can check `Inertia.render("Users/Index", …)` against it). The name
12
+ // Inertia hands us is a runtime string, so this one lookup widens it.
13
+ const page = (pages as Record<string, () => Promise<{ default: unknown }>>)[name];
11
14
  if (!page) throw new Error(`Inertia page not found: "${name}"`);
12
15
  return (await page()).default as DefineComponent;
13
16
  },
@@ -5,7 +5,7 @@
5
5
  // Bun.build with splitting:true creates one .js chunk per page.
6
6
  // Converting to static imports will bundle ALL pages into app.js.
7
7
 
8
- export const pages: Record<string, () => Promise<{ default: unknown }>> = {
8
+ export const pages = {
9
9
  about: () => import("./pages/about.vue"),
10
10
  contact: () => import("./pages/contact.vue"),
11
11
  error: () => import("./pages/error.vue"),
@@ -15,4 +15,13 @@ export const pages: Record<string, () => Promise<{ default: unknown }>> = {
15
15
  profile: () => import("./pages/profile.vue"),
16
16
  register: () => import("./pages/register.vue"),
17
17
  "reset-password": () => import("./pages/reset-password.vue"),
18
- };
18
+ } satisfies Record<string, () => Promise<{ default: unknown }>>;
19
+
20
+ // Type-only: lets a controller's `Inertia.render(name, props)` be checked
21
+ // against the props each page component declares. Erased at runtime — and
22
+ // it is the ONLY place the server side touches the client component graph.
23
+ declare module "@zerotal/inertia" {
24
+ interface InertiaPageRegistry {
25
+ pages: typeof pages;
26
+ }
27
+ }
@@ -15,3 +15,18 @@ export interface SharedProps {
15
15
  old: Record<string, unknown>;
16
16
  [key: string]: unknown;
17
17
  }
18
+
19
+ /**
20
+ * The same list, told to the server side. `Inertia.render(page, props)` checks a
21
+ * controller's props against the page component's own props — so without this,
22
+ * a page that reads `auth` through `usePage()` would look to the server like a
23
+ * page whose controller forgot to pass it.
24
+ *
25
+ * `share()` is a runtime call in a provider, so nothing can generate this: when
26
+ * you register a new shared prop, add it in both places.
27
+ */
28
+ declare module "@zerotal/inertia" {
29
+ interface SharedProps {
30
+ auth: { user: { id: number; name: string; email: string } | null };
31
+ }
32
+ }
@@ -21,6 +21,7 @@
21
21
  ]
22
22
  }
23
23
  },
24
+ "include": ["**/*"],
24
25
  "exclude": [
25
26
  "node_modules",
26
27
  ".zerotal"