@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.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. 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.