@zerotal/arch 1.8.1 → 1.10.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.
Files changed (64) hide show
  1. package/api-surface.md +6 -6
  2. package/docs/admin/actions.md +15 -0
  3. package/docs/admin/auth.md +10 -0
  4. package/docs/admin/dashboard.md +12 -0
  5. package/docs/admin/extending-ui.md +14 -0
  6. package/docs/admin/forms.md +15 -0
  7. package/docs/admin/operations.md +12 -0
  8. package/docs/admin/resources.md +6 -0
  9. package/docs/admin/tables.md +21 -0
  10. package/docs/audit.md +5 -0
  11. package/docs/authentication.md +110 -1
  12. package/docs/broadcasting/references.md +17 -0
  13. package/docs/cache.md +5 -0
  14. package/docs/carbon.md +5 -0
  15. package/docs/changelog.md +281 -0
  16. package/docs/client/index.md +17 -0
  17. package/docs/commands.md +6 -0
  18. package/docs/components.md +73 -0
  19. package/docs/config-system.md +54 -0
  20. package/docs/cookies.md +6 -0
  21. package/docs/deployment.md +151 -13
  22. package/docs/devtools.md +5 -0
  23. package/docs/email-verification.md +26 -1
  24. package/docs/encryption.md +21 -0
  25. package/docs/errors.md +2 -0
  26. package/docs/flow/components.md +54 -0
  27. package/docs/flow/forms.md +57 -0
  28. package/docs/flow/references.md +14 -0
  29. package/docs/getting-started.md +38 -0
  30. package/docs/health.md +19 -0
  31. package/docs/helpers.md +150 -0
  32. package/docs/i18n.md +5 -0
  33. package/docs/inertia/middleware.md +44 -0
  34. package/docs/inertia/props.md +70 -0
  35. package/docs/inertia/ssr.md +95 -10
  36. package/docs/lock.md +15 -0
  37. package/docs/logger.md +38 -0
  38. package/docs/middleware.md +31 -0
  39. package/docs/migrations.md +47 -0
  40. package/docs/monitor.md +59 -0
  41. package/docs/notifications.md +11 -0
  42. package/docs/orm/casts.md +6 -0
  43. package/docs/orm/lifecycle.md +18 -0
  44. package/docs/orm/queries.md +10 -0
  45. package/docs/orm/relationships.md +30 -0
  46. package/docs/queue.md +10 -0
  47. package/docs/rate-limiting.md +84 -21
  48. package/docs/responses.md +23 -0
  49. package/docs/routing.md +16 -0
  50. package/docs/scheduler.md +82 -8
  51. package/docs/session.md +6 -0
  52. package/docs/social.md +10 -0
  53. package/docs/storage.md +21 -0
  54. package/docs/support-policy.md +13 -1
  55. package/docs/telemetry.md +8 -0
  56. package/docs/tenancy.md +6 -0
  57. package/docs/testing/index.md +105 -0
  58. package/docs/upgrade.md +48 -0
  59. package/docs/validator.md +9 -0
  60. package/docs/view.md +6 -0
  61. package/package.json +3 -3
  62. package/src/install/guidelines.ts +1 -1
  63. package/src/mcp/stdio.ts +3 -3
  64. package/src/tools/_probe.ts +2 -2
package/docs/changelog.md CHANGED
@@ -27,6 +27,287 @@ the section for every version you cross and apply its migration notes, not only
27
27
  majors. [Releases and versioning](/docs/support-policy#releases-and-versioning) explains
28
28
  when that carve-out ends.
29
29
 
30
+ ## 1.10.0 — 2026-08-30
31
+
32
+ A second report from the team building on Zerotal, and the things it found. Most of this
33
+ release is failures that were silent by construction — mail delivered nowhere, a page
34
+ shared as a grey rectangle, a schedule that never fired, a rate limiter with one bucket
35
+ for everybody. None of them logged anything.
36
+
37
+ **Three things to know before upgrading.**
38
+
39
+ - **React apps using SSR now need `@inertiajs/react` installed.** It is the same adapter
40
+ your browser entry point already uses; the server renders through its `<App>` so
41
+ `<Head>` works. A missing one is a named error rather than a silent omission.
42
+ - **`scheduler.timezone` does something now.** It was documented as informational and read
43
+ by nothing. Its default moved from the literal `"UTC"` to **the system zone**, so an app
44
+ that never set the key keeps doing exactly what it did — but an app that set it now gets
45
+ what it asked for. If you set it to `"UTC"` on a server that is not on UTC, your
46
+ schedules will move. See [the upgrade guide](/docs/upgrade#1-9-to-1-10).
47
+ - **Named rate limiters need `.trustedProxies(n)` behind a proxy.** `.byIp()`, `.byUser()`
48
+ and `.byApiKey()` now ignore `X-Forwarded-For` unless told how many proxies sit in
49
+ front, which is the same rule `ThrottleMiddleware` already followed. `zt doctor` reports
50
+ any that need it.
51
+
52
+ ### Added
53
+
54
+ - **`zt assets:prune`** — removes the chunks an earlier release left behind, on the machine
55
+ that never ran a build. `assets:build --clean` cleans the directory it _builds into_,
56
+ which does nothing for the usual release shape: build here, tar the output, extract it
57
+ over `public/` there. Extracting merges, so every deploy adds another set of
58
+ content-hashed chunks and none are ever removed. One app reached 225 chunk files for the
59
+ 49 its entry point references. Ship `.zerotal/` with the release and this removes what
60
+ the build record does not claim. See [Deployment](/docs/deployment#assetsprune--clearing-up-after-the-extract-instead-of-before-it).
61
+
62
+ - **`zt deploy:<env> --check`** — the preflight gate on its own, for the point in a release
63
+ script where the new code is on disk and the service has not restarted. Exit 0 and
64
+ restart; exit non-zero and keep serving the previous release. Everything that can refuse
65
+ already runs by the end of preflight and none of it mutates, so stopping there is a
66
+ complete answer rather than half a deploy.
67
+
68
+ - **`RateLimiter.trustedProxies(n)`** on the fluent builder, and
69
+ **`res.assertInertiaRedirect(url)`** in `@zerotal/testing` — the assertion that checks
70
+ what actually breaks on an Inertia redirect, which is the `X-Inertia` marker rather than
71
+ the status and `Location` a normal redirect assertion already covers.
72
+
73
+ - **`@zerotal/core/runtime`** (`zerotal/runtime`) — the runtime checks as exports, so a
74
+ script or a test can make the same assertion `zt` makes: `runtimeBelowFloor`,
75
+ `declaredBunFloor`, `runtimeMismatch`, `bunBinary` and the messages that go with them.
76
+
77
+ - **`definedOnly()` and `Resolved<T>`** on `@zerotal/core/helpers`, for merging an options
78
+ bag over defaults without an explicit `undefined` overwriting one.
79
+
80
+ - **Scheduler timezone helpers** — `wallClockIn`, `isValidTimeZone`,
81
+ `CronExpression.matchesIn` and `CronExpression.nextRunAfterIn`, plus `SchedulerError` and
82
+ `UnknownTimeZoneError`.
83
+
84
+ - **A boot line when a convention is skipped in this environment.** An env-restricted
85
+ concern is skipped by _not looking_, which is correct and completely silent: an app ran
86
+ for weeks in production with `app/schedules` full and no worker process, and nothing
87
+ logged anything because from a web process's point of view nothing existed.
88
+
89
+ ### Changed
90
+
91
+ - **Optional properties in public option shapes are declared `?: T | undefined`.** The
92
+ generated `tsconfig.json` enables `exactOptionalPropertyTypes`, under which
93
+ `image?: string` refuses a key that is present and holds `undefined` — so
94
+ `{ image: candidate ?? undefined }`, the most ordinary thing there is, did not compile
95
+ and every conditionally-absent field had to be spelled `...(x ? { x } : {})`. 438
96
+ properties across 115 files. Nothing changes for a reader: an absent optional property
97
+ already read as `undefined`.
98
+
99
+ - **`scheduler.timezone` is honoured**, and its default is the system zone rather than the
100
+ literal `"UTC"`. See the note above.
101
+
102
+ - **`mail.driver: "log"` fails `zt doctor` in production** when `mail.from.address` has been
103
+ configured, and warns when it is still the placeholder. Mail written to a log file is
104
+ delivered to nobody and says so nowhere.
105
+
106
+ ### Fixed
107
+
108
+ - **React SSR emitted no `<Head>` tags at all.** The React branch rendered the page
109
+ component directly, and `<Head>` renders nothing — it reports its children to a head
110
+ manager it reads from context, and rendering the component alone puts none there. So
111
+ every page served the template's `<head>`: no title, no description, no card. Nothing
112
+ failed and nothing logged, because the page is perfect in a browser, where React has run.
113
+ Only the readers that do not run JavaScript saw it — which is every link-preview scraper
114
+ and every `curl`.
115
+
116
+ - **SMTP submission on port 587 sent nothing.** The STARTTLS handshake completed and then
117
+ the client's `EHLO` was dropped: `upgradeTLS()` returns the new socket while the
118
+ handshake is still in flight, and a write issued in that window is lost — not buffered,
119
+ not an error, gone. Port 465 was unaffected, so mail worked on the port nobody documents
120
+ and the 587 every provider _does_ document produced silence: no error, no bounce, no log
121
+ line, and password resets that never arrived.
122
+
123
+ - **TLS certificates were not actually verified, on either SMTP transport.**
124
+ `rejectUnauthorized` is not enforced by the runtime — it reports a self-signed
125
+ certificate as authorized and puts the real reason beside it — so the connection was
126
+ encrypted and would have accepted that encryption from anyone in the network path. The
127
+ driver reads the handshake result itself now and fails closed.
128
+
129
+ - **A scheduled task with a `timezone` took the whole scheduler down.** `Bun.cron`'s options
130
+ form throws, and it throws during registration, so the worker died on boot and
131
+ restart-looped: one task with a timezone stopped every task in the app. Zerotal evaluates
132
+ the zone itself now, and a task that cannot register takes only itself out.
133
+
134
+ - **Named rate limiters ignored `trustedProxies`, and `zt doctor` was told not to look.**
135
+ `.byIp()`, `.byUser()` and `.byApiKey()` used a resolver that read the socket address and
136
+ fell back to the leftmost `X-Forwarded-For` entry with no proxy count. Behind a reverse
137
+ proxy every visitor keyed on the proxy's own address and shared one bucket, so a `login`
138
+ limiter of five attempts a minute was five attempts a minute for the entire user base and
139
+ one attacker locked everybody out. The doctor check written to catch this exempted any
140
+ custom `keyResolver`, which is what all three are.
141
+
142
+ - **`ctx.session.intended()` could not read what `AuthMiddleware` stored.** It used the key
143
+ `intended` while the middleware and `redirect().intended()` used `intended_url`. Each pair
144
+ was internally consistent and separately tested, so every test passed — and an app that
145
+ mixed them, which the documentation invited, was silently sent to `/` after every sign-in.
146
+
147
+ - **`MonitorStore` overwrote its own defaults with `undefined`.** It applied `?? …` defaults
148
+ and then spread `...opts` after them, and spread copies own properties even when they
149
+ hold `undefined` — so an unset config put `undefined` back over the retention window and
150
+ `prune()` computed a `NaN` cutoff, pruning nothing and reporting nothing.
151
+
152
+ - **`engines.bun` is enforced.** Every generated app writes a floor and nothing read it.
153
+ `Intl` output moves between Bun releases, so a suite with currency or date assertions goes
154
+ red on a runtime that is otherwise fine and the failures name the code they touch rather
155
+ than the binary.
156
+
157
+ - **The asset build record is portable.** Its filename was hashed from the output
158
+ directory's _absolute_ path, so a record shipped with a release matched nothing at the
159
+ other end and moving a checkout silently orphaned it.
160
+
161
+ - **The React SSR root is marked `data-server-rendered`**, so the client hydrates the markup
162
+ instead of discarding it and rendering the page a second time. `POST /__ssr` returns the
163
+ same body shape as the Vue branch.
164
+
165
+ ### Documented
166
+
167
+ - **["What a crawler sees"](/docs/inertia/ssr#what-a-crawler-sees)** — `inertia()` does not
168
+ server-render the component, which is the normal Inertia arrangement and worth saying out
169
+ loud: the served document is a `<title>` and a JSON blob. Which readers run JavaScript,
170
+ which do not, and the three ways to give the second group something to read.
171
+
172
+ - **[Which Inertia redirects are covered](/docs/inertia/middleware#which-redirects-are-covered)**
173
+ — all of them, because `useOnce()` registers the middleware globally. Written down because
174
+ the opposite belief is what keeps an app's own workaround on every request forever.
175
+
176
+ - **[`bun test` vs `bun zt test`](/docs/testing#bun-test-vs-bun-zt-test)** — the 30-second
177
+ timeout (the `bunfig.toml` key is ignored by Bun and `setDefaultTimeout()` in a preload
178
+ reaches only the first file, so the flag is the only mechanism that works), the runtime
179
+ check, and the fact that configuration resolves once per process — so a test that mutates
180
+ the environment in `beforeAll` is testing whichever file booted first.
181
+
182
+ - **[Timezones](/docs/scheduler#timezones)** in the scheduler, **[the middleware names the
183
+ framework occupies](/docs/middleware#names-the-framework-already-occupies)**, and why
184
+ `X-Forwarded-For` is [counted from the right](/docs/rate-limiting#trustedproxies).
185
+
186
+ ## 1.9.0 — 2026-08-29
187
+
188
+ The gaps an app was filling in for itself: one Bun per project, a database backup that is not
189
+ `cp`, a release gate the pipeline will actually call, a boundary between a model and a page
190
+ prop, and helpers that work on both sides of the wire.
191
+
192
+ **Two things to know before upgrading.** Both are new refusals or new noise, and both are quiet
193
+ if they do not apply to you.
194
+
195
+ - **`zt` now refuses to run when a project has two Bun runtimes in it** — the shell's `bun` and
196
+ a different one in `node_modules`. If it fires, pick one: `bun update bun` moves the installed
197
+ copy to match your shell, or run everything through `node_modules/.bin/bun`. To boot anyway
198
+ while you sort it out, set `ZT_ALLOW_RUNTIME_MISMATCH=1`. Most projects never see this,
199
+ because most have no `bun` in `node_modules` to disagree with.
200
+ - **Passing an ORM model straight into an Inertia page prop now warns in development**, once per
201
+ model class, if that model declares neither `hidden` nor `visible`. Declaring either silences
202
+ it — and is the fix, not the silencer. Production is unaffected.
203
+
204
+ ### Added
205
+
206
+ - **One project, one Bun.** `engines.bun` is a floor and nothing enforced it, so an app could be
207
+ served by one runtime and tested on another — the shell's `bun` and a `node_modules/bun` put
208
+ there by a transitive peer dependency nobody declared. A green suite is then evidence about a
209
+ binary the app is not served by. `startZerotal()` refuses on a mismatch, and `zt test` spawns
210
+ the binary running it rather than a name `PATH` resolves.
211
+
212
+ - **`zt db:backup`** — a verified snapshot of the SQLite database, using `VACUUM INTO` rather
213
+ than `cp`. Copying a live SQLite file can capture a half-written page and produce a backup
214
+ that restores as a corrupt database, months later, from the one file you were relying on.
215
+ Every snapshot is opened and integrity-checked as it is written, `--require-rows` fails a
216
+ backup whose business tables are empty, `--rehearse` performs the actual restore, and every
217
+ failure path exits non-zero — a backup timer that reports success while writing nothing is
218
+ worse than no timer at all. See [Deployment](/docs/deployment#back-up-the-database).
219
+
220
+ - **`DeployTarget.preflight`** — a slot for the app's own release gate, run after the config
221
+ validators and `doctor` and before anything is built or migrated. A command named
222
+ `release:check` is found by convention, with nothing to wire up. A declared name that is not
223
+ registered **fails** the deploy rather than being skipped: a gate nothing calls is a comment.
224
+
225
+ - **`zerotal/shared`** — the helpers with no server in them, importable from a browser bundle:
226
+ `pluralize`, `Str`, and new `formatMoney` / `formatNumber` / `formatDate`. A total that reads
227
+ `R 39 147` on screen and `R39,147.00` on the invoice looks like two different numbers to the
228
+ person paying it, and maintaining that in two files is how it happens. See
229
+ [Helpers](/docs/helpers#sharing-helpers-with-the-browser--zerotalshared).
230
+
231
+ - **`<form data-enhance>`** — a plain server-rendered page, with no Flow component on it, can
232
+ submit without the page flashing. It posts through `fetch` and the matching form in the
233
+ response replaces it in place, so a validation error lands where the person is looking. Its
234
+ own dependency-free bundle at `/__flow/enhance.js`, added with `flowEnhanceTag()` in the
235
+ layout. Every path degrades: a network failure re-submits natively, a redirect is followed and
236
+ `pushState`d, and no JavaScript at all is an ordinary form post.
237
+
238
+ - **Three new `doctor` checks.** A rate limiter that cannot tell two people apart behind a proxy
239
+ — where the socket address is the proxy's for every request, so one attacker can lock out
240
+ everybody. Auth columns missing from a table a migration built without them, which otherwise
241
+ surfaces as `no such column` in tests that have nothing to do with email. And migrations that
242
+ have not run, named, before a request finds out.
243
+
244
+ ### Changed
245
+
246
+ - **A model reaching Inertia page props says what it is safe to publish.** Page props are page
247
+ source, and `return inertia("Trips/Show", { trip })` ships every column of the row — the
248
+ internal cost, the margin, the note about the customer, on the customer's own screen. The
249
+ ORM's `hidden` / `visible` lists were already honoured and nothing said so. See the upgrade
250
+ note above and [Inertia props](/docs/inertia/props#page-props-are-page-source).
251
+
252
+ - **A bound field the model will not accept says so.** `flow:model` on a column missing from
253
+ `fillable` was dropped in silence: the form submitted, nothing was written, nothing failed.
254
+ The drop stays — the same path receives whatever a browser sends — but a developer's typo no
255
+ longer produces the same silence as a hostile payload. Development only, once per field.
256
+
257
+ - **INTERNAL: 116 exports leave the recorded API surface** across `core`, `orm`, `flow`,
258
+ `admin`, `flow-ui` and `monitor`. **Nothing is removed and nothing breaks** — they are still
259
+ exported and still work; what changes is the promise. The dev orchestrator, the ORM's
260
+ connection wiring and dialect layer, the admin panel's page machinery and Flow's
261
+ wire-protocol frame types are not things an app constructs, and naming them in a `stable`
262
+ surface implied a guarantee about a protocol that is free to change. Each package's own
263
+ changelog lists its share.
264
+
265
+ - **A minor breaks nothing that can wait.** The roadmap used to say a minor never breaks
266
+ anything, which was false when written — three breaks had already shipped in minors, each
267
+ deliberately, each with a note, exactly as the [support policy](/docs/support-policy) has
268
+ always described. An absolute rule the project knowingly broke is worse than an honest one.
269
+
270
+ ### Documented
271
+
272
+ - **Every promised export is documented — 100%, up from 60%.** `maturity: stable` means an
273
+ export keeps its shape for the rest of the 1.x line, and the gate measuring how much of that
274
+ promise was written down stood at 798 gaps. It is zero.
275
+
276
+ Four features turned out to have shipped and been invisible. **Passkeys** — `PasskeyService`
277
+ has been here since 1.7.0 with no page at all, including that `requireUserVerification`
278
+ defaults to `true` because that is what makes a passkey a second factor rather than one.
279
+ **`@zerotal/core/env`**, a typed environment schema that reports every bad variable at once
280
+ rather than one per restart. **The outbound `Http` client**, which the testing guide had been
281
+ linking to a page that did not describe it. And **`@zerotal/monitor`'s Export JSON**, where the
282
+ button was documented and the forty-odd row types it hands you were not.
283
+
284
+ Also named for the first time: `@zerotal/flow-ui`'s sixty-one component prop types, which a
285
+ wrapper component cannot be written without.
286
+
287
+ The gate itself could not see `.tsx` files: with `jsx` unset, TypeScript declines to pull such
288
+ a module into the program rather than failing to parse it, so every symbol in one was
289
+ invisible. It had been inflating exactly the TSX-heavy packages.
290
+
291
+ ### Fixed
292
+
293
+ - **A rebuilt Inertia bundle no longer 404s on a chunk the browser asks for.**
294
+ `resources/js/app.tsx` builds to `/assets/app.js` under that name every time, while
295
+ `splitting: true` names each chunk after its content. A rebuild therefore rewrites `app.js` to
296
+ import `chunk-NEW.js` and prunes `chunk-OLD.js` — and a browser holding a cached `app.js` asks
297
+ for the pruned one:
298
+
299
+ GET /assets/chunk-hrnspqda.js status=404
300
+
301
+ from a page that renders and a server that is healthy, with nothing in that line leading back
302
+ to the template.
303
+
304
+ The template hardcodes `/assets/app.js` rather than calling `asset()`, so the version token the
305
+ rest of the framework appends never reached it — and cache-busting had only ever been
306
+ implemented for `serve --dev`. It now applies in every environment: the file's mtime in dev,
307
+ where a rebuild happens without a restart, and the boot-derived asset version otherwise. An
308
+ unchanged asset keeps a stable URL and stays cached, which is why the token is derived rather
309
+ than random.
310
+
30
311
  ## 1.8.1 — 2026-08-26
31
312
 
32
313
  DevTools showed you the wrong request, accurately.
@@ -328,6 +328,23 @@ This package also ships `Socket`, `Channel` and `PresenceChannel` — a WebSocke
328
328
  client speaking Zerotal's broadcast protocol. Different job, documented where it is
329
329
  used: [Broadcasting → Client](/docs/broadcasting/client).
330
330
 
331
+ ## Types
332
+
333
+ The typed client derives its shape from your app's routes, so most of these are inferred rather
334
+ than written by hand — but a helper that wraps a call needs to name them:
335
+
336
+ | Type | What it is |
337
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
338
+ | `ApiRouteMap` | The generated route table the client is typed against. |
339
+ | `RouteShape` | One route's method, params, body and response. |
340
+ | `PathsFor<M>` | Every path answering a given method — what makes an unknown path a compile error. |
341
+ | `PathParams`, `ParamRecord` | The params a path captures. |
342
+ | `BodyOf<P>`, `QueryOf<P>`, `ResponseOf<P>` | One route's request body, query and response types. |
343
+ | `ResponseMeta` | Status, headers and timing carried alongside a response. |
344
+ | `RequestInterceptor`, `ResponseInterceptor` | Hooks running before a request leaves and after one returns — auth headers, retry, logging. |
345
+ | `CircuitBreakerOptions` | When the client stops calling an endpoint that keeps failing, and when it tries again. |
346
+ | `SocketOptions`, `SocketState`, `SocketLike` | The realtime socket: how it connects, where it is in its lifecycle, and the minimum interface a substitute must satisfy. |
347
+
331
348
  ## Next steps
332
349
 
333
350
  - [Rate Limiting](/docs/rate-limiting) — throttle what you send and what you accept.
package/docs/commands.md CHANGED
@@ -370,10 +370,16 @@ can gate a deploy.
370
370
  | `bun zt migrate:status` | Show the status (run / pending / batch) of each migration file |
371
371
  | `bun zt migrate:generate` | Auto-generate a migration from model schema changes |
372
372
  | `bun zt db:seed` | Run all seeders from `database/seeders/` |
373
+ | `bun zt db:backup` | Take a verified SQLite snapshot with `VACUUM INTO` |
373
374
 
374
375
  > **Danger** — `migrate --fresh` and `migrate:fresh` drop every table before
375
376
  > re-running migrations. Never run them against a production database.
376
377
 
378
+ `db:backup` is the counterpart to everything above it: those commands all assume the database
379
+ file will still be there. It verifies every snapshot it writes and exits non-zero on any
380
+ failure, so it is safe to run from a timer — see
381
+ [Deployment](/docs/deployment#back-up-the-database) for the flags and a systemd unit.
382
+
377
383
  ### Generators
378
384
 
379
385
  | Command | Creates |
@@ -2348,6 +2348,79 @@ Or import directly from the package: `import { Prose } from "@zerotal/flow-ui";`
2348
2348
 
2349
2349
  <!-- END GENERATED COMPONENTS -->
2350
2350
 
2351
+ ## Types
2352
+
2353
+ Every component exports the type of its own props, named after it — `Button` takes
2354
+ `ButtonProps`, `Dialog` takes `DialogProps`. Reach for them when you wrap a component rather
2355
+ than use it directly, which is the usual reason an app needs one:
2356
+
2357
+ ```tsx fragment
2358
+ import { Button, type ButtonProps } from "@zerotal/flow-ui";
2359
+
2360
+ /** Our save button: the same API, one decision already made. */
2361
+ export function SaveButton(props: Omit<ButtonProps, "variant">) {
2362
+ return <Button variant="primary" {...props} />;
2363
+ }
2364
+ ```
2365
+
2366
+ The full set:
2367
+
2368
+ ```text
2369
+ AccordionProps AlertDialogProps AlertProps AlertTextProps
2370
+ AspectRatioProps AvatarProps BadgeProps BreadcrumbProps
2371
+ ButtonGroupProps ButtonProps CalendarProps CardElementProps
2372
+ CarouselProps ChartProps CheckboxProps CollapsibleProps
2373
+ ComboboxProps CommandProps ContextMenuProps DatePickerProps
2374
+ DialogProps DropdownMenuItemProps DropdownMenuLabelProps
2375
+ DropdownMenuProps DropdownMenuSeparatorProps DropdownMenuShortcutProps
2376
+ EmptyProps FieldProps GvaProps HoverCardProps
2377
+ InputGroupProps InputOTPProps InputProps ItemProps
2378
+ KbdProps LabelProps MenubarProps NavigationMenuProps
2379
+ PaginationProps PopoverProps ProgressProps ProseProps
2380
+ RadioGroupProps ResizableProps ScrollAreaProps SelectProps
2381
+ SeparatorProps SheetProps SidebarProps SkeletonProps
2382
+ SliderProps SpinnerProps SwitchProps TableProps
2383
+ TabsProps TextProps TextareaProps ToasterProps
2384
+ ToggleGroupProps ToggleProps TooltipProps
2385
+ ```
2386
+
2387
+ ### Values a prop takes
2388
+
2389
+ Several props are unions rather than free strings, and the union is exported so a variable
2390
+ holding one can be typed:
2391
+
2392
+ | Type | Used by |
2393
+ | --------------------- | ----------------------------------------------------------- |
2394
+ | `SelectOption` | `Select`, `Combobox` — one `{ label, value }` entry. |
2395
+ | `RadioOption` | `RadioGroup`. |
2396
+ | `TabItem` | `Tabs`. |
2397
+ | `SidebarItem` | `Sidebar`. |
2398
+ | `TableGroup` | `Table` — a grouped set of rows. |
2399
+ | `NavigationPanelLink` | `NavigationMenu`. |
2400
+ | `PopoverSide` | `Popover`, `Tooltip`, `HoverCard` — which side it opens on. |
2401
+ | `PopoverAlign` | The same, for alignment. |
2402
+ | `ToastPosition` | `Toaster` — where toasts stack. |
2403
+ | `ChartType` | `Chart`. |
2404
+ | `ClassValue` | Anything the `class` prop accepts, before merging. |
2405
+
2406
+ ### Sub-components
2407
+
2408
+ A few components are used as a small set rather than one tag: `AlertTitle` and
2409
+ `AlertDescription` inside `Alert`, `DropdownMenuShortcut` inside a menu item. The prose
2410
+ components — `Blockquote`, `Lead`, `H2`, `H3`, `H4` — are the same idea for `Prose`.
2411
+
2412
+ ### Variants and theming
2413
+
2414
+ Each variant-bearing component exports its own `cva` config, so a wrapper can reuse the exact
2415
+ class set rather than re-deriving it: `buttonVariants`, `badgeVariants`, `alertVariants`,
2416
+ `spinnerVariants`, `toggleVariants`, and `popoverSurface` for the floating surfaces.
2417
+ `GvaConfig` is the shape they are built from.
2418
+
2419
+ Theming is `flowTokensCss` (the CSS custom properties), `flowTailwindConfig` (the Tailwind
2420
+ preset that reads them), and `FlowUiThemeConfig` for `config/flow-ui.ts`. `THEME`,
2421
+ `THEME_STORAGE_KEY` and `THEME_TOGGLE_SCRIPT` are the light/dark toggle: the script runs before
2422
+ first paint, so a page does not flash the wrong theme on load.
2423
+
2351
2424
  ## Testing
2352
2425
 
2353
2426
  Set your suite up once as described in [Testing](/docs/testing). A `flow-ui`
@@ -42,6 +42,45 @@ const apiKey = env("API_KEY"); // no fallback → string | undefined
42
42
 
43
43
  > **Note** — When you need a value to be present, use `requireEnv("APP_KEY")` instead — it throws a `ConfigError` at boot if the variable is unset, rather than returning `undefined`.
44
44
 
45
+ ### Declaring the whole environment — `EnvSchema`
46
+
47
+ `env()` is per-call and forgiving: an unset variable is `undefined` and you find out where it is
48
+ used. `@zerotal/core/env` is the other end — declare every variable the app reads, once, and the
49
+ boot either produces a fully typed frozen object or fails with every problem listed at the same
50
+ time:
51
+
52
+ ```typescript fragment
53
+ // env.ts
54
+ import { EnvSchema, t } from "@zerotal/core/env";
55
+
56
+ export const env = EnvSchema.define({
57
+ APP_KEY: t.string().min(32),
58
+ PORT: t.port().default(3000),
59
+ DATABASE_URL: t.string(),
60
+ LOG_LEVEL: t.enum(["debug", "info", "warn", "error"]).default("info"),
61
+ SENTRY_DSN: t.url().optional(),
62
+ });
63
+
64
+ env.PORT; // number — never undefined, because it has a default
65
+ env.LOG_LEVEL; // "debug" | "info" | "warn" | "error", narrowed to the literals
66
+ ```
67
+
68
+ **It reports every failure at once.** A schema with three missing variables fails the boot
69
+ naming all three, rather than one per restart — which is the difference between one fix and
70
+ three round trips through a deploy. The failure is an `EnvSchemaError` carrying an
71
+ `EnvFieldError` per field.
72
+
73
+ | Type | What it is |
74
+ | ---------------- | --------------------------------------------------------------------------- |
75
+ | `EnvSchemaError` | The boot failure, listing every field that did not validate. |
76
+ | `EnvFieldError` | One field's problem: which variable, and what was wrong with it. |
77
+ | `FieldType` | The builders `t` offers — string, number, boolean, port, url, enum. |
78
+ | `EnvOutput<S>` | The typed object a schema produces. `typeof env` where you need to pass it. |
79
+ | `InferDef<D>` | The type one field definition resolves to. |
80
+
81
+ Use `env()` for a value read in one place and `EnvSchema` for the set an app cannot start
82
+ without. They coexist; the schema is not a replacement for the helper.
83
+
45
84
  ## Config files
46
85
 
47
86
  Config files live in `config/`. Each file exports a typed object via a package helper:
@@ -250,6 +289,21 @@ if (config("app.env") === "production") {
250
289
  | `ConfigLoader.get` | `get(key: string, fallback?): value` | Dot-path read against the loaded map. |
251
290
  | `ConfigLoader.validate` | `validate(): this` | Run each file's optional `validate(config)` export, throwing on failure. |
252
291
 
292
+ ## Types
293
+
294
+ | Type | What it is |
295
+ | ------------------- | -------------------------------------------------------------------------------------- |
296
+ | `ConfigValidator` | What `registerConfigValidator` takes — a function handed the config, reporting issues. |
297
+ | `ConfigIssue` | One finding: its namespace, message, and level. |
298
+ | `ConfigIssueLevel` | Whether an issue refuses a production boot or is only worth reading. |
299
+ | `ConventionsConfig` | The `conventions` namespace — where the framework looks for models, jobs and the rest. |
300
+ | `AppTlsConfig` | TLS settings under `app.tls`. |
301
+ | `AssetLoaderKind` | How an asset is loaded by the build — the `loader` values `assets.loaders` accepts. |
302
+
303
+ A validator reporting a **fatal** issue refuses a production-like boot rather than warning. That
304
+ is the whole point of the level: an app that boots with a broken configuration serves wrong
305
+ answers rather than failing, and the failure is the cheaper outcome.
306
+
253
307
  ## Next steps
254
308
 
255
309
  - [Conventions](/docs/conventions) — the auto-discovery settings under the `conventions` key.
package/docs/cookies.md CHANGED
@@ -93,6 +93,12 @@ You rarely set cookies by hand — two parts of the framework manage their own:
93
93
  (non-`HttpOnly`) cookie after every request so Axios/Inertia can echo it back as
94
94
  the `X-XSRF-TOKEN` header.
95
95
 
96
+ ## Types
97
+
98
+ `CookieOptions` is what every cookie-setting call accepts — `maxAge`, `path`, `domain`,
99
+ `httpOnly`, `secure`, `sameSite`. It is exported so a helper that sets a cookie in more than one
100
+ place can take the same shape.
101
+
96
102
  ## Next steps
97
103
 
98
104
  - [Session](/docs/session) — signed, `HttpOnly` cookie-backed state (the usual choice).