@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.
- package/api-surface.md +6 -6
- package/docs/admin/actions.md +15 -0
- package/docs/admin/auth.md +10 -0
- package/docs/admin/dashboard.md +12 -0
- package/docs/admin/extending-ui.md +14 -0
- package/docs/admin/forms.md +15 -0
- package/docs/admin/operations.md +12 -0
- package/docs/admin/resources.md +6 -0
- package/docs/admin/tables.md +21 -0
- package/docs/audit.md +5 -0
- package/docs/authentication.md +110 -1
- package/docs/broadcasting/references.md +17 -0
- package/docs/cache.md +5 -0
- package/docs/carbon.md +5 -0
- package/docs/changelog.md +281 -0
- package/docs/client/index.md +17 -0
- package/docs/commands.md +6 -0
- package/docs/components.md +73 -0
- package/docs/config-system.md +54 -0
- package/docs/cookies.md +6 -0
- package/docs/deployment.md +151 -13
- package/docs/devtools.md +5 -0
- package/docs/email-verification.md +26 -1
- package/docs/encryption.md +21 -0
- package/docs/errors.md +2 -0
- package/docs/flow/components.md +54 -0
- package/docs/flow/forms.md +57 -0
- package/docs/flow/references.md +14 -0
- package/docs/getting-started.md +38 -0
- package/docs/health.md +19 -0
- package/docs/helpers.md +150 -0
- package/docs/i18n.md +5 -0
- package/docs/inertia/middleware.md +44 -0
- package/docs/inertia/props.md +70 -0
- package/docs/inertia/ssr.md +95 -10
- package/docs/lock.md +15 -0
- package/docs/logger.md +38 -0
- package/docs/middleware.md +31 -0
- package/docs/migrations.md +47 -0
- package/docs/monitor.md +59 -0
- package/docs/notifications.md +11 -0
- package/docs/orm/casts.md +6 -0
- package/docs/orm/lifecycle.md +18 -0
- package/docs/orm/queries.md +10 -0
- package/docs/orm/relationships.md +30 -0
- package/docs/queue.md +10 -0
- package/docs/rate-limiting.md +84 -21
- package/docs/responses.md +23 -0
- package/docs/routing.md +16 -0
- package/docs/scheduler.md +82 -8
- package/docs/session.md +6 -0
- package/docs/social.md +10 -0
- package/docs/storage.md +21 -0
- package/docs/support-policy.md +13 -1
- package/docs/telemetry.md +8 -0
- package/docs/tenancy.md +6 -0
- package/docs/testing/index.md +105 -0
- package/docs/upgrade.md +48 -0
- package/docs/validator.md +9 -0
- package/docs/view.md +6 -0
- package/package.json +3 -3
- package/src/install/guidelines.ts +1 -1
- package/src/mcp/stdio.ts +3 -3
- 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.
|
package/docs/client/index.md
CHANGED
|
@@ -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 |
|
package/docs/components.md
CHANGED
|
@@ -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`
|
package/docs/config-system.md
CHANGED
|
@@ -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).
|