@zerotal/arch 1.7.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 +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- package/src/tools/searchDocs.ts +345 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Release Notes
|
|
3
|
+
description: What changed in each tagged Zerotal release, and the steps needed to upgrade.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Release Notes
|
|
7
|
+
|
|
8
|
+
Releases are recorded below, newest first. The `@zerotal/*` packages share a
|
|
9
|
+
single version line and follow [semantic versioning](/docs/upgrade#versioning).
|
|
10
|
+
Each package also keeps a detailed `CHANGELOG.md` of its own; this page is the
|
|
11
|
+
summary across the suite.
|
|
12
|
+
|
|
13
|
+
> **Tip** — For the mechanics of moving between versions — bumping packages, running migrations, and re-checking config — see the [Upgrade Guide](/docs/upgrade).
|
|
14
|
+
|
|
15
|
+
## How to read these notes
|
|
16
|
+
|
|
17
|
+
Each version lists changes under three headings:
|
|
18
|
+
|
|
19
|
+
- **Added** — new features and APIs (safe to adopt incrementally).
|
|
20
|
+
- **Changed** — behavior changes; **breaking** ones are called out explicitly and
|
|
21
|
+
appear only in major releases.
|
|
22
|
+
- **Fixed** — bug fixes.
|
|
23
|
+
|
|
24
|
+
Patch and minor releases are backward compatible. Before taking a **major** release,
|
|
25
|
+
read its section here and apply each migration note.
|
|
26
|
+
|
|
27
|
+
## 1.7.0 — 2026-08-16
|
|
28
|
+
|
|
29
|
+
The agent surface, a DevTools panel that shows the framework and not just the last request,
|
|
30
|
+
and the repayment of four things the 1.x line had promised without delivering.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`@zerotal/arch` — an MCP server that hands a coding agent the framework's own truth.**
|
|
35
|
+
Not a documentation search over prose about an API: `api_surface` returns the exact
|
|
36
|
+
TypeScript signature of every export, read from the version installed in your project and
|
|
37
|
+
diffed by CI on every change. Alongside it, `search_docs` over the documentation that
|
|
38
|
+
shipped with that same version, `routes` and `schema` read from the live router and the
|
|
39
|
+
models' own metadata, `logs`/`last_error` from the app's structured trail, `baselines`, and
|
|
40
|
+
`doctor` — the one an agent is meant to finish a task with, because every finding carries
|
|
41
|
+
its fix.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bun add -d @zerotal/arch
|
|
45
|
+
bun zt arch:install # writes .mcp.json, AGENTS.md, and a CLAUDE.md shim
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Re-running is safe: every generated region is marker-fenced, so `arch:update` on your next
|
|
49
|
+
upgrade replaces what it wrote and leaves anything you added around it alone. Ships `beta`.
|
|
50
|
+
See [Agent Surface](/docs/arch).
|
|
51
|
+
|
|
52
|
+
- **DevTools grew an App section.** Every surface until now read the request stream — what one
|
|
53
|
+
request did. Six new tabs behind a **Requests | App** switch answer what the app _is_:
|
|
54
|
+
routes, resolved config with secrets masked, container bindings and which provider bound
|
|
55
|
+
each, provider boot cost, event listeners, and console commands with scheduled tasks. Every
|
|
56
|
+
location in the panel is now a link into your editor.
|
|
57
|
+
|
|
58
|
+
- **Security headers cover static files.** Files under `public/` are handed to Bun as
|
|
59
|
+
pre-registered responses and served without entering JavaScript, so no middleware ever ran
|
|
60
|
+
for them — every asset went out with no `X-Content-Type-Options: nosniff`, the response
|
|
61
|
+
class sniffing protection exists for. The header set is baked into the compiled response, so
|
|
62
|
+
Bun still serves the file natively.
|
|
63
|
+
|
|
64
|
+
- **`zt doctor --url` reports security headers sent twice.** A header your app sets and your
|
|
65
|
+
proxy also sets is invisible from inside the process. Conflicting values fail the check —
|
|
66
|
+
browsers do not agree which copy applies, so the control is enforced inconsistently —
|
|
67
|
+
and identical duplicates warn.
|
|
68
|
+
|
|
69
|
+
- **`DeepPartial<T>`**, exported from the kernel. `deepMerge` does a deep merge and its
|
|
70
|
+
parameter said `Partial<T>`, which only makes the top level optional — so overriding one
|
|
71
|
+
field of a nested config block was a type error against a merge that handles it perfectly.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
- **Migrations are now actually transactional.** The runner wrapped each `up()` in a
|
|
76
|
+
transaction and the docblock promised all-or-nothing, but the wrapper governed nothing:
|
|
77
|
+
`Schema` resolved the _global_ connection, so a migration's DDL ran on a pooled connection
|
|
78
|
+
and committed independently. On PostgreSQL, a migration failing on its third statement left
|
|
79
|
+
the first two behind and the `ROLLBACK` had nothing to undo. DDL now joins the enclosing
|
|
80
|
+
transaction, the tracking-table row is written inside it, and rollback carries the same
|
|
81
|
+
guarantee. MySQL has no transactional DDL, so the runner no longer opens one there and
|
|
82
|
+
`zt migrate` says so before it starts. See
|
|
83
|
+
[Migrations → What happens when a migration fails](/docs/migrations#what-happens-when-a-migration-fails).
|
|
84
|
+
|
|
85
|
+
- **`BaseMiddleware.with()` type-checks its options.** Its options type was inferred from the
|
|
86
|
+
object literal it was handed rather than from the middleware class, so the literal was
|
|
87
|
+
checked against itself: every callback parameter arrived implicitly `any`, and a misspelled
|
|
88
|
+
option was accepted in silence.
|
|
89
|
+
|
|
90
|
+
- **SPA navigation no longer leaks the outgoing page's state script.** The swap removed the
|
|
91
|
+
first `flow-state-*` element in document order, which on any page with a child island was
|
|
92
|
+
the island's, not the page's. The orphans accumulated one per navigation for as long as the
|
|
93
|
+
tab stayed open.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- **DDL issued inside `DB.transaction()` now joins that transaction.** Previously
|
|
98
|
+
`Schema.create()` and friends resolved the global connection and committed separately. This
|
|
99
|
+
is the fix above, and it applies to any code — not only migrations — that issues DDL inside
|
|
100
|
+
a transaction.
|
|
101
|
+
|
|
102
|
+
- **`Component._skipMount` is gone** (`@internal`). It was written by `hydrate()` and read by
|
|
103
|
+
nothing; mount-skipping is structural, and `$refresh`/`$mount` deliberately re-mount a
|
|
104
|
+
hydrated page, so honouring the flag would have broken both. `hooks.test.ts` pins the real
|
|
105
|
+
guarantee — mount runs exactly once per session.
|
|
106
|
+
|
|
107
|
+
## 1.6.3 — 2026-08-15
|
|
108
|
+
|
|
109
|
+
Two guards against the same failure: an upgrade sitting on disk while something older keeps
|
|
110
|
+
running, with nothing on screen to say so.
|
|
111
|
+
|
|
112
|
+
### Added
|
|
113
|
+
|
|
114
|
+
- **`serve --dev` reports a framework upgrade it has not picked up.** A running dev server
|
|
115
|
+
holds the code it imported at boot, so `bun add zerotal@latest` in another terminal changes
|
|
116
|
+
`node_modules` and nothing else — a save restarts only the worker, against the same
|
|
117
|
+
in-memory framework. The upgrade therefore appears to do nothing. The supervisor now names
|
|
118
|
+
both versions and says to restart, and the dev banner carries the version it is running:
|
|
119
|
+
`Zerotal v1.6.3 › dev`.
|
|
120
|
+
|
|
121
|
+
- **`create-zerotal` says when it is not the published scaffolder.** `bun create zerotal` can
|
|
122
|
+
serve a copy cached from an earlier run, and a stale scaffolder stamps the dependency ranges
|
|
123
|
+
_it_ shipped with — so a brand-new project is created against versions that are no longer
|
|
124
|
+
current, while the install log shows today's framework resolving inside those ranges. It now
|
|
125
|
+
checks the registry and names the fix: `bunx create-zerotal@latest <name>`. Advisory only —
|
|
126
|
+
offline, firewalled and slow all mean "no answer", and no answer never stops anyone creating
|
|
127
|
+
an app.
|
|
128
|
+
|
|
129
|
+
## 1.6.2 — 2026-08-15
|
|
130
|
+
|
|
131
|
+
### Fixed
|
|
132
|
+
|
|
133
|
+
- **`serve --dev` now stops its worker on Windows instead of killing it.** Restarting sent
|
|
134
|
+
`SIGTERM`, which Windows has no way to deliver — there the call terminates the process
|
|
135
|
+
where it stands, so on every save no provider drained, no open response was finished and
|
|
136
|
+
no database handle was closed. The supervisor asks over an IPC channel now and only kills
|
|
137
|
+
if that goes unanswered. Nothing changes on macOS or Linux beyond the mechanism.
|
|
138
|
+
|
|
139
|
+
- **The devtools panel no longer fills the console with network errors.** Its event stream
|
|
140
|
+
was abandoned on shutdown rather than closed, leaving the browser with a truncated
|
|
141
|
+
response and a `net::ERR_INCOMPLETE_CHUNKED_ENCODING` for every reload. The stream is
|
|
142
|
+
closed properly now, and a heartbeat keeps an idle one from being dropped with nothing
|
|
143
|
+
written for either end to notice by.
|
|
144
|
+
|
|
145
|
+
## 1.6.1 — 2026-08-15
|
|
146
|
+
|
|
147
|
+
### Fixed
|
|
148
|
+
|
|
149
|
+
- **The Inertia DevTools panel said the app was not in dev mode**, and suggested starting a
|
|
150
|
+
Vite dev server — advice that cannot be followed in a Zerotal app. The cause was real
|
|
151
|
+
though: the Inertia adapter turns its client-side hooks on from a `dev` option that
|
|
152
|
+
defaults to `import.meta.env.DEV`, a Vite convention that Bun's bundler leaves alone, so
|
|
153
|
+
it survived into the bundle and evaluated to `false` on every build.
|
|
154
|
+
|
|
155
|
+
Zerotal now defines `import.meta.env` — `DEV`, `PROD` and `MODE` — for every bundled
|
|
156
|
+
browser build. Nothing to configure and no `dev` option to pass by hand; rebuild and the
|
|
157
|
+
panel works. See [Inertia DevTools](/docs/inertia/devtools).
|
|
158
|
+
|
|
159
|
+
### Changed
|
|
160
|
+
|
|
161
|
+
- **New React and Vue apps scaffold Inertia 3.** The panel's client half — visit options,
|
|
162
|
+
prefetch-cache entries, and the grouping that tells a poll apart from a navigation — exists
|
|
163
|
+
only in the version 3 adapters, and neither template needed a single edit to build against
|
|
164
|
+
it. Existing apps are unaffected; `bun add @inertiajs/react@^3` (or `@inertiajs/vue3@^3`)
|
|
165
|
+
is the whole upgrade if you want the client half.
|
|
166
|
+
|
|
167
|
+
## 1.6.0 — 2026-08-15
|
|
168
|
+
|
|
169
|
+
### Added
|
|
170
|
+
|
|
171
|
+
- **`route()` works in the browser.** The typed helper now has a twin at `zerotal/routes`.
|
|
172
|
+
Hand it the table `bun zt route:types` already generates, once, at your entry point, and
|
|
173
|
+
`route("posts.show", { slug })` works in a component exactly as it does in a controller.
|
|
174
|
+
`hasRoute(name)` answers the conditional-link question without a try/catch.
|
|
175
|
+
|
|
176
|
+
The two are one implementation, not two that agree today: param encoding, catch-all
|
|
177
|
+
handling and every error message live in a shared builder, and only the table lookup
|
|
178
|
+
differs — the live router on the server, the generated map in the browser. A parity test
|
|
179
|
+
asserts they emit byte-identical URLs and identical error text. See
|
|
180
|
+
[Routing](/docs/routing).
|
|
181
|
+
|
|
182
|
+
- **`$route()` in Flow's Alpine expressions** — `<a :href="$route('posts.show', { slug })">`,
|
|
183
|
+
with nothing to install. Inertia apps import their table; `/__flow/runtime.js` is built by
|
|
184
|
+
the framework rather than your app, so the runtime handler serialises the table onto the
|
|
185
|
+
bundle it serves instead. Same builder as the server, so a link written in an Alpine
|
|
186
|
+
expression and one written in JSX cannot disagree about encoding.
|
|
187
|
+
|
|
188
|
+
- **Inertia DevTools.** A server-side recorder for the Inertia DevTools browser extension:
|
|
189
|
+
requests, resolved props, and which wrapper produced each one. Off unless the process
|
|
190
|
+
already exposes dev surfaces — the same gate as the stack-trace error page — and an app
|
|
191
|
+
that enables it without saying who may read it gets a 403 rather than an open endpoint.
|
|
192
|
+
Redaction runs before storage, so a withheld value is never written down. See
|
|
193
|
+
[Inertia DevTools](/docs/inertia/devtools).
|
|
194
|
+
|
|
195
|
+
### Fixed
|
|
196
|
+
|
|
197
|
+
- **Ten more places asked `APP_ENV` a question it cannot answer**, found by auditing every
|
|
198
|
+
reader rather than waiting for the next report. `APP_ENV` holds the runtime mode once the
|
|
199
|
+
app has booted, so a check comparing it against a deployment name was asking whether
|
|
200
|
+
`"web"` is production. The consequences were real:
|
|
201
|
+
|
|
202
|
+
- **auto-`synchronize` was never hard-off in production** — the only thing between a
|
|
203
|
+
production database and boot-time schema sync was the config default;
|
|
204
|
+
- **the Flow client bundle was never minified in production**, shipping ~183 KB
|
|
205
|
+
unminified to every visitor;
|
|
206
|
+
- **`forceState()`** did not refuse to run on live data;
|
|
207
|
+
- **environment-scoped scheduled tasks never ran** — `.environments(["production"])`
|
|
208
|
+
matched nothing, silently;
|
|
209
|
+
- the admin environment badge showed `web` on every screen, so the one mistake it exists
|
|
210
|
+
to prevent — editing production believing it is staging — was exactly what it could not
|
|
211
|
+
prevent.
|
|
212
|
+
|
|
213
|
+
All read the deployment name now, and every one still fails closed. Reading `APP_ENV`
|
|
214
|
+
directly is a lint error from this release, because fourteen instances of one mistake
|
|
215
|
+
across seven packages were each found separately.
|
|
216
|
+
|
|
217
|
+
- **`useOnce()` no longer demands a cast.** Registering middleware from a provider required
|
|
218
|
+
`useOnce(Middleware as never)` in all eight packages that do it — a cast the framework was
|
|
219
|
+
asking for. Twelve of them are gone, and the casting-debt baseline came down with them.
|
|
220
|
+
|
|
221
|
+
## 1.5.1 — 2026-08-15
|
|
222
|
+
|
|
223
|
+
### Fixed
|
|
224
|
+
|
|
225
|
+
- **Development surfaces were switching themselves off.** A scaffolded app with
|
|
226
|
+
`APP_ENV=development` in its `.env` got **production error pages** from `bun zt serve`, and
|
|
227
|
+
**DevTools never appeared at all** — in any app, in any mode. The admin panel's development
|
|
228
|
+
bypass and the monitor's open-by-default access were dead for the same reason.
|
|
229
|
+
|
|
230
|
+
All of them asked `APP_ENV` whether this was a development environment, but `setAppEnv()`
|
|
231
|
+
replaces that variable with the runtime mode (`web`, `console`, `worker`) before the app is
|
|
232
|
+
created — so the question being asked was whether `"web"` is development. They read the
|
|
233
|
+
preserved deployment name now. Production and staging are unaffected: every one of these
|
|
234
|
+
gates still fails closed, and an unset environment still fails closed.
|
|
235
|
+
|
|
236
|
+
If you upgrade and suddenly see the DevTools panel, that is the fix, not a new feature.
|
|
237
|
+
|
|
238
|
+
## 1.5.0 — 2026-08-15
|
|
239
|
+
|
|
240
|
+
The largest release of the 1.x line: a new package, three features, a batch of
|
|
241
|
+
production-hardening work that came out of a real deployment, and the last of the
|
|
242
|
+
packages reaching `stable`. Of the 26 published packages, **25 are `stable` and one
|
|
243
|
+
is `experimental`** (`@zerotal/ai`); none is `beta`.
|
|
244
|
+
|
|
245
|
+
### Added
|
|
246
|
+
|
|
247
|
+
- **`bun zt deploy:<env>` — a release that refuses to finish when something is wrong.**
|
|
248
|
+
Four phases, ordered so that **everything that can refuse runs before anything that
|
|
249
|
+
mutates**: preflight (is this really that environment, would this config refuse a
|
|
250
|
+
production boot, does `zt doctor` pass), build, migrate, verify. It exits non-zero and
|
|
251
|
+
does not restart your service — systemd or your container runtime owns that, and this
|
|
252
|
+
gives it a gate to restart behind. Every environment gets its own command;
|
|
253
|
+
`production` and `staging` exist without configuration, and `config/deploy.ts` declares
|
|
254
|
+
more. The target name is checked against the deployment the process was started as, so
|
|
255
|
+
`deploy:production` on a staging box stops before it migrates the wrong database.
|
|
256
|
+
`--dry-run`, `--skip-migrations` and `--probe` are there. See
|
|
257
|
+
[Deployment](/docs/deployment).
|
|
258
|
+
- **`zt doctor` checks CORS and HSTS.** `app.cors.origin: "*"` lets any site read your
|
|
259
|
+
responses out of a visitor's browser; `app.secureHeaders.secure` gates HSTS and
|
|
260
|
+
defaults to off. Both now fail on a production-like deployment.
|
|
261
|
+
- **`@zerotal/ai` — a typed agent loop, shipping `experimental`.** One loop shared by every
|
|
262
|
+
driver, so switching models is a config change rather than a rewrite. A `pause_turn` is
|
|
263
|
+
resumed rather than mistaken for an answer; a refusal is a typed outcome checked before
|
|
264
|
+
anything reads the content; schema translation decides what a provider can express instead
|
|
265
|
+
of hoping. Named agent runs take a refreshable lock, spend ceilings and prompt redaction are
|
|
266
|
+
first-class, and `AiFake` makes the whole thing testable without a network. It ships
|
|
267
|
+
`experimental` deliberately — the surface is expected to move inside 1.x, and the
|
|
268
|
+
[support policy](/docs/support-policy) says what that means. See [AI](/docs/ai).
|
|
269
|
+
- **Typed route names — `bun zt route:types`.** The command boots the app, reads the routes it
|
|
270
|
+
actually registered, and writes `types/routes.generated.ts`. With it, `route("psots.show")` is
|
|
271
|
+
a compile error and `route("posts.show", {})` names the `slug` it wants. Params come from the
|
|
272
|
+
pattern, so adding a segment updates every call site. It boots rather than scanning `routes/`
|
|
273
|
+
because a route name comes from three places and only one of them is a file path. See
|
|
274
|
+
[Routing](/docs/routing).
|
|
275
|
+
- **Typed Inertia pages.** `Inertia.render(component, props)` is checked against the page
|
|
276
|
+
component's own props, and the prop wrappers (`defer`, `optional`, `always`) are generic, so a
|
|
277
|
+
renamed or retyped prop fails at the render call rather than in the browser. See
|
|
278
|
+
[Inertia](/docs/inertia).
|
|
279
|
+
- **The development error page can say what to do, not just what broke.** `no such table: assets`
|
|
280
|
+
is exact about the failure and useless about the cause — every frame in its stack sits inside
|
|
281
|
+
the SQL driver. `registerErrorDiagnoser()` lets the package that owns an error contribute a
|
|
282
|
+
diagnosis above the stack; `@zerotal/orm` registers the first one, turning a missing table into
|
|
283
|
+
the list of migrations that have not run, with a button to run them. See [Errors](/docs/errors).
|
|
284
|
+
- **`bun zt dev` — the server and every companion process in one terminal**, with the Deck, a
|
|
285
|
+
tabbed dev UI that adds no dependency. The queue worker runs as its own tab. A service provider
|
|
286
|
+
contributes its own checks through `doctorChecks()`. See [Devtools](/docs/devtools).
|
|
287
|
+
- **Flow: `<ErrorBoundary>`, `stream`, `<SectionContent>` / `<SectionOutlet>`, and `<Virtualize>`.**
|
|
288
|
+
A failing child now costs that child rather than the page; a slow child no longer holds up the
|
|
289
|
+
shell; a page can fill a region its layout owns; and a collection too large for the DOM gets a
|
|
290
|
+
scrolling window over it. `@zerotal/flow/browser` drives a real browser against a running app,
|
|
291
|
+
and a compiled-versus-runtime parity suite keeps the two renderers honest. See
|
|
292
|
+
[Flow](/docs/flow).
|
|
293
|
+
- **ORM: `migrate:refresh`, and `--seed` on `migrate` / `migrate:fresh`.** See
|
|
294
|
+
[Migrations](/docs/migrations).
|
|
295
|
+
- **Queue: debounced jobs.** `debounce` on a `Job` collapses repeated dispatches into one run.
|
|
296
|
+
See [Queue](/docs/queue).
|
|
297
|
+
- **Scheduler: durable run history**, so the monitor panel survives a restart. See
|
|
298
|
+
[Scheduler](/docs/scheduler).
|
|
299
|
+
- **Media: `allowEnlargement` on a conversion, and `@zerotal/media/testing`.** `ImageDriver` is
|
|
300
|
+
frozen, with its growth rule written down. See [Media](/docs/media).
|
|
301
|
+
|
|
302
|
+
### Changed
|
|
303
|
+
|
|
304
|
+
Most of this section is one body of work: the response to a Flow field report, hardening the path
|
|
305
|
+
from a local machine to a deployed box.
|
|
306
|
+
|
|
307
|
+
- **`app.allowedOrigins` is declared config and defaults to the origin of `app.url`.** The
|
|
308
|
+
common deployment no longer needs to configure it at all, and the setting is visible where the
|
|
309
|
+
rest of the app's URL configuration lives rather than being implied.
|
|
310
|
+
- **`bun zt doctor --url=…` probes a deployed transport from the outside.** It reports what each
|
|
311
|
+
transport path actually answers over the wire, which is the question a failing WebSocket
|
|
312
|
+
upgrade in production actually raises. `Application.declareWebSocketPath()` / `webSocketPaths()`
|
|
313
|
+
let a package declare its own path so the probe covers it, and Flow declares `/__flow/ws` at
|
|
314
|
+
registration. See [Deployment](/docs/deployment).
|
|
315
|
+
- **`serve` no longer rebuilds assets at boot in production**, and Flow no longer rebuilds its
|
|
316
|
+
CSS/JS bundles at boot, when the output directory is read-only. A read-only tree is normal for
|
|
317
|
+
a container image, and building at boot turned it into a crash. `bun zt assets:build` is the
|
|
318
|
+
explicit build step to run before deploying. See [Assets](/docs/assets).
|
|
319
|
+
- **The Flow client says which transport failure it hit** rather than failing the same way for
|
|
320
|
+
every cause, and `data-flow-connection` is stamped on a page that connected normally — so
|
|
321
|
+
"is it live?" is answerable from the DOM.
|
|
322
|
+
- **`route()` takes query values as a third argument** — `route(name, params, query)` — and
|
|
323
|
+
`route.dynamic(name, params?, query?)` covers a name that is not known at compile time.
|
|
324
|
+
- **`ctx.user` is typed as `UserModel`**, the same interface `Auth.user()` returns.
|
|
325
|
+
- **`SessionContract.get` and `pull` take an optional `<T>`.**
|
|
326
|
+
- **`withoutOverlapping`'s cross-process lock defaults to 5 minutes, not 24 hours.** A worker
|
|
327
|
+
killed mid-run used to block its own schedule for the rest of the day.
|
|
328
|
+
- **`app/commands/` is auto-discovered**, and boot warns about a `routes/` directory nothing
|
|
329
|
+
routes.
|
|
330
|
+
- **Thirteen packages reached `stable`** — `admin`, `audit`, `broadcasting`, `devtools`, `flow`,
|
|
331
|
+
`flow-ui`, `i18n`, `inertia`, `media`, `monitor`, `notifications`, `telemetry` and `tenancy`
|
|
332
|
+
— each after documenting its remaining exports and marking its plumbing `@internal`. The
|
|
333
|
+
component reference now documents all 53 `flow-ui` components and cannot drift again.
|
|
334
|
+
|
|
335
|
+
### Fixed
|
|
336
|
+
|
|
337
|
+
- **Flow: a decorator could be registered against the wrong component.** Field decorators cannot
|
|
338
|
+
see their own class, so each registration is buffered and matched to a class afterwards — and
|
|
339
|
+
the match searched one flat buffer by field name. A component that declares a field and is never
|
|
340
|
+
rendered leaves its entry there for the life of the process, so an unrelated component with a
|
|
341
|
+
field of the same name could claim it and never receive its own. It showed up as `@reactive`
|
|
342
|
+
silently failing to register, which remounts the child on every parent-pushed change rather than
|
|
343
|
+
updating it in place. Matching is now per declaring class, and on the fields a class declares
|
|
344
|
+
rather than everything on an instance.
|
|
345
|
+
- **Flow: a keyless child in a list was identified by its position.** Reordering a list without
|
|
346
|
+
keys reused the wrong DOM node, so state attached to a row followed the position rather than
|
|
347
|
+
the row.
|
|
348
|
+
- **Flow: a client expression that writes an `@expose` prop now syncs to the server.**
|
|
349
|
+
- **ORM: a `Date` in a query-builder write was silently discarded.**
|
|
350
|
+
- **ORM: altering a Postgres column silently dropped its `NOT NULL` and `DEFAULT`**, and SQLite
|
|
351
|
+
now refuses an impossible `dropColumn` before applying anything rather than partway through.
|
|
352
|
+
- **ORM: the N+1 detector reads the bindings, not just the SQL text**, so it stops missing
|
|
353
|
+
queries that differ only in their parameters.
|
|
354
|
+
- **Cache: stampede protection survives a compute slower than 30 seconds.**
|
|
355
|
+
- **Media: `fit: "cover"` works on the default driver**, `fit: "inside"` returns the dimensions
|
|
356
|
+
it promised, and `fit: "fill"` with a single dimension behaves as `inside`. Both shipped
|
|
357
|
+
drivers are held to one parity suite.
|
|
358
|
+
- **`serve --dev` built a Flow app's bundles three times on every start**, and dev asset builds
|
|
359
|
+
are now skipped when nothing changed.
|
|
360
|
+
- **A weak `APP_KEY` never refused a production boot**, and **N+1 detection ran in
|
|
361
|
+
production**. Both asked `Bun.env.APP_ENV` whether this was production — but that
|
|
362
|
+
variable holds the runtime mode (`web`, `console`, `worker`) by the time anything
|
|
363
|
+
reads it, so both always got "no". The deployment name is now preserved and read
|
|
364
|
+
back through `deployEnv()`.
|
|
365
|
+
- **`staging` was production for some things and not others** — config validation
|
|
366
|
+
refused an insecure staging boot, while assets went out unminified and were rebuilt
|
|
367
|
+
at boot, which is exactly the combination that restart-loops on a hardened unit.
|
|
368
|
+
- **`app.secureHeaders` only allowed `frameOptions` to be configured**, so there was
|
|
369
|
+
no supported way to turn HSTS on. Every option the middleware reads is now typed.
|
|
370
|
+
- **`setAppEnv("dev")` resolved to `console` rather than `web`.**
|
|
371
|
+
|
|
372
|
+
## 1.4.0 — 2026-08-10
|
|
373
|
+
|
|
374
|
+
### Added
|
|
375
|
+
|
|
376
|
+
- **ORM: encrypted columns.** A column can hold ciphertext at rest and plaintext on the model,
|
|
377
|
+
keyed by `APP_KEY` with AES-256-GCM — `@column("encrypted") idNumber?: string`, or
|
|
378
|
+
`static encryptable = ["idNumber", "passportNumber"]` for several at once. Unlike `hashable`
|
|
379
|
+
this is reversible and does not touch the instance, so the property still reads as plaintext
|
|
380
|
+
after `save()`. `where()` on an encrypted column throws rather than matching nothing (a fresh
|
|
381
|
+
IV per write means the ciphertext never repeats), and a value the key cannot open fails the
|
|
382
|
+
read rather than arriving somewhere as ciphertext. See [Casts & Mutators](/docs/orm/casts).
|
|
383
|
+
- **Auth: `TwoFactor.getQrCodeSvg()`** renders the two-factor enrolment QR code as an inline
|
|
384
|
+
`<svg>`, drawn in-process. The `otpauth://` URI carries the TOTP secret, so the previous advice
|
|
385
|
+
— hand it to a QR image service — posted the second factor to a third party. `encodeQr()` and
|
|
386
|
+
`qrSvg()` are exported for drawing the symbol yourself. See [Roles & 2FA](/docs/roles-and-2fa).
|
|
387
|
+
- **Flow: `preserveScroll`** on `<Link>` and `navigateCurrent()`, for a sort header, filter or tab
|
|
388
|
+
strip partway down a page that should not jump to the top.
|
|
389
|
+
|
|
390
|
+
### Fixed
|
|
391
|
+
|
|
392
|
+
- **Flow: `flow:navigate` did not scroll.** The SPA swap replaced the page under a stationary
|
|
393
|
+
viewport, so following a link from near the bottom of a long list landed you halfway down the
|
|
394
|
+
next page — which reads as the page having failed to load. A navigation now goes to the top (or
|
|
395
|
+
to the URL's fragment), and Back and Forward restore where you were.
|
|
396
|
+
- **Flow: `focusOnError` did nothing on a runtime-rendered page.** The JSX runtime rewrote the
|
|
397
|
+
hyphen in `flow:focus-error` to a dot, so the attribute never matched the selector the client
|
|
398
|
+
looks for. It worked on a compiled page and silently did not on one the compiler bailed out of.
|
|
399
|
+
`sortGroupId` was affected the same way.
|
|
400
|
+
- **Docs: two column examples named the wrong TypeScript type.** `@column("date")` hydrates a
|
|
401
|
+
native `Date`, not a `Carbon`, and `decimal:N` surfaces as a `string` — the ORM overview typed
|
|
402
|
+
both the other way, which `tsc` cannot catch because the decorator does not constrain the
|
|
403
|
+
property type.
|
|
404
|
+
|
|
405
|
+
## 1.3.0 — 2026-08-09
|
|
406
|
+
|
|
407
|
+
### Changed — BREAKING
|
|
408
|
+
|
|
409
|
+
- **Mixin composition is now a static on the base class.** `ComponentWith(...)` and
|
|
410
|
+
`BaseModelWith(...)` are removed; write `Component.using(Pagination)` and
|
|
411
|
+
`Model.using(Authenticatable, Roles)` instead. A codemod ships in the repository
|
|
412
|
+
(`scripts/codemod-mixin-composition.ts`) that rewrites call sites and imports. How mixins are
|
|
413
|
+
_authored_ is unchanged. `using` also composes onto intermediate bases
|
|
414
|
+
(`AdminPage.using(Pagination)`) and chains (`.using(a).using(b)`), neither of which the old
|
|
415
|
+
helpers could express.
|
|
416
|
+
- **`Model` is the canonical ORM base-class name.** `BaseModel` remains exported as an alias for
|
|
417
|
+
the same class, so existing code keeps working; docs and scaffolding now say
|
|
418
|
+
`class User extends Model`.
|
|
419
|
+
|
|
420
|
+
### Added
|
|
421
|
+
|
|
422
|
+
- **`@zerotal/media`** — attach files to models with `Model.using(Media)`: collections with
|
|
423
|
+
acceptance rules and retention, image conversions on `Bun.Image` (or `sharp`), responsive
|
|
424
|
+
`srcset()` ladders with inline placeholders, queued conversion jobs, `MediaFake` test
|
|
425
|
+
assertions, and `media:clean` / `media:regenerate` commands. See [Media Library](/docs/media).
|
|
426
|
+
|
|
427
|
+
### Fixed
|
|
428
|
+
|
|
429
|
+
- **Flow: an `@expose`d action on a shared page base could vanish from the action allowlist**
|
|
430
|
+
(and be fatally rejected at runtime) whenever a subclass declared a decorated field — a Bun
|
|
431
|
+
1.3.x decorator defect, worked around in the framework. `@expose`, `@task`, `@renderless`,
|
|
432
|
+
`@on` and `@computed` were all affected.
|
|
433
|
+
|
|
434
|
+
## 1.1.0 — 2026-08-08
|
|
435
|
+
|
|
436
|
+
### Changed
|
|
437
|
+
|
|
438
|
+
- `FlowTest.call()` rethrows action errors and `FlowTest.set()` re-renders, so tests fail on
|
|
439
|
+
broken actions instead of passing silently. A handler pointing at an un-`@expose`d method is
|
|
440
|
+
now a build error (fatal at boot in CSP-safe mode).
|
|
441
|
+
- `@column("text")` maps to a real `TEXT` type rather than `VARCHAR` — affects newly generated
|
|
442
|
+
tables and migrations only.
|
|
443
|
+
|
|
444
|
+
### Fixed
|
|
445
|
+
|
|
446
|
+
- Radio-group binding, reactive sibling attributes suppressing `value` bindings, modifier click
|
|
447
|
+
handlers, `request().ip()` inside actions, a data-corrupting `json` cast on numeric-looking
|
|
448
|
+
strings, and an unparseable `make:model` stub.
|
|
449
|
+
|
|
450
|
+
## 1.0.4 — 2026-08-07
|
|
451
|
+
|
|
452
|
+
- Fixed the Flow starter rendering unstyled (stylesheet path mismatch) and its missing favicon.
|
|
453
|
+
|
|
454
|
+
## 1.0.3 — 2026-08-06
|
|
455
|
+
|
|
456
|
+
- Re-released so npm build provenance resolves against the renamed repository.
|
|
457
|
+
|
|
458
|
+
## 1.0.2 and earlier — 2026-08-06
|
|
459
|
+
|
|
460
|
+
- First published versions of Zerotal.
|
|
461
|
+
|
|
462
|
+
## Next steps
|
|
463
|
+
|
|
464
|
+
- [Upgrade Guide](/docs/upgrade) — apply the migration notes for a new release.
|
|
465
|
+
- [Contributing](/docs/contributing) — how changes land before they reach this list.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Client Authentication
|
|
3
|
+
description: Bearer tokens, CSRF, and refreshing credentials on a 401.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Authentication & CSRF
|
|
7
|
+
|
|
8
|
+
The client supports the two ways a browser app proves who it is, and the choice is
|
|
9
|
+
usually made for you by where the API lives:
|
|
10
|
+
|
|
11
|
+
- **Bearer tokens** suit APIs on another origin, mobile clients, and anything where
|
|
12
|
+
the caller holds a credential it can attach itself.
|
|
13
|
+
- **Session cookies** suit an API served from your own domain, where the browser
|
|
14
|
+
already carries the session and CSRF protection is the concern instead.
|
|
15
|
+
|
|
16
|
+
## Bearer tokens
|
|
17
|
+
|
|
18
|
+
Attach a bearer token (string or a resolver, sync or async) without writing an interceptor —
|
|
19
|
+
update it at runtime with `setToken()`:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// app/api/client.ts
|
|
23
|
+
const api = createApiClient<Routes>({
|
|
24
|
+
token: () => authStore.accessToken, // re-read on every request
|
|
25
|
+
});
|
|
26
|
+
api.setToken(freshToken); // or update imperatively
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Prefer the resolver form. A plain string is captured once at construction, so a
|
|
30
|
+
token refreshed later never reaches the client; a function is consulted on every
|
|
31
|
+
request and always sees the current value.
|
|
32
|
+
|
|
33
|
+
Calling `setToken()` with no argument clears the token, which is what a logout
|
|
34
|
+
should do — otherwise the next request still carries the credential of the user who
|
|
35
|
+
just signed out.
|
|
36
|
+
|
|
37
|
+
> **Note** — The `token` is only applied when no `Authorization` header is already
|
|
38
|
+
> present on the request, so a per-request override always wins.
|
|
39
|
+
|
|
40
|
+
## Session cookies and CSRF
|
|
41
|
+
|
|
42
|
+
For session/cookie (SPA) auth, set `withCredentials` to send cookies, which also turns on CSRF:
|
|
43
|
+
the client reads the `XSRF-TOKEN` cookie and sends it as `X-XSRF-TOKEN` on mutating requests
|
|
44
|
+
(matching the session/CSRF middleware). Customize the names with `csrf`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// app/api/client.ts
|
|
48
|
+
createApiClient<Routes>({
|
|
49
|
+
withCredentials: true, // credentials: 'include' + CSRF on
|
|
50
|
+
csrf: { cookie: "XSRF-TOKEN", header: "X-XSRF-TOKEN" }, // defaults shown
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Enabling `withCredentials` turns CSRF on by default, so the two travel together and
|
|
55
|
+
neither needs configuring in the common case. Set `csrf: false` to opt out, or pass
|
|
56
|
+
an object to rename the cookie and header to match a server that uses different
|
|
57
|
+
ones.
|
|
58
|
+
|
|
59
|
+
The token is attached only to mutating requests — `POST`, `PUT`, `PATCH`, `DELETE`.
|
|
60
|
+
A `GET` is exempt because it should not change state, so it needs no protection
|
|
61
|
+
from being triggered cross-site. If a `GET` in your API does change something, that
|
|
62
|
+
is the thing to fix; adding a CSRF header to it would only hide the problem.
|
|
63
|
+
|
|
64
|
+
The header is skipped when the request already carries one, so a caller that sets
|
|
65
|
+
its own value keeps it.
|
|
66
|
+
|
|
67
|
+
## 401 / token refresh
|
|
68
|
+
|
|
69
|
+
`onUnauthorized` is called when any request receives a 401 response. It receives
|
|
70
|
+
the error and a `retry` function. Call `retry()` — optionally with header overrides
|
|
71
|
+
— to re-execute the failed request. The retry is limited to **one attempt**.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// app/api/client.ts
|
|
75
|
+
const api = createApiClient<Routes>({
|
|
76
|
+
baseUrl: "https://api.example.com",
|
|
77
|
+
|
|
78
|
+
onUnauthorized: async (err, retry) => {
|
|
79
|
+
const newToken = await authStore.refresh();
|
|
80
|
+
return retry({ Authorization: `Bearer ${newToken}` });
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If `onUnauthorized` is not provided or does not call `retry`, the 401 error is
|
|
86
|
+
thrown normally.
|
|
87
|
+
|
|
88
|
+
The single-attempt limit is deliberate: a refresh that itself returns 401 would
|
|
89
|
+
otherwise retry forever, turning an expired session into an endless loop of
|
|
90
|
+
requests. When the retry also fails, the error is thrown and the app can send the
|
|
91
|
+
user to the login screen.
|
|
92
|
+
|
|
93
|
+
One case the hook does not solve on its own is a page that fires several requests
|
|
94
|
+
at once. Each 401 calls `onUnauthorized` separately, so a naive handler triggers
|
|
95
|
+
several concurrent refreshes and the losers of that race may invalidate the winner's
|
|
96
|
+
token. Have the refresh itself de-duplicate — cache the in-flight promise in your
|
|
97
|
+
auth store and hand the same one to every caller until it settles:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// app/api/authStore.ts
|
|
101
|
+
let inflight: Promise<string> | null = null;
|
|
102
|
+
|
|
103
|
+
export function refresh(): Promise<string> {
|
|
104
|
+
inflight ??= requestNewToken().finally(() => (inflight = null));
|
|
105
|
+
return inflight;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Next steps
|
|
110
|
+
|
|
111
|
+
- [Client overview](/docs/client) — the guide's front page and the rest of the sections.
|
|
112
|
+
- [Error handling](/docs/client/errors) — the errors a rejected request throws.
|
|
113
|
+
- [CSRF protection](/docs/csrf) — the server side of the cookie and header pair.
|