@zerotal/arch 1.8.0 → 1.9.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 (59) hide show
  1. package/docs/admin/actions.md +15 -0
  2. package/docs/admin/auth.md +10 -0
  3. package/docs/admin/dashboard.md +12 -0
  4. package/docs/admin/extending-ui.md +14 -0
  5. package/docs/admin/forms.md +15 -0
  6. package/docs/admin/operations.md +12 -0
  7. package/docs/admin/resources.md +6 -0
  8. package/docs/admin/tables.md +21 -0
  9. package/docs/audit.md +5 -0
  10. package/docs/authentication.md +110 -1
  11. package/docs/broadcasting/references.md +17 -0
  12. package/docs/cache.md +5 -0
  13. package/docs/carbon.md +5 -0
  14. package/docs/changelog.md +159 -1
  15. package/docs/client/index.md +17 -0
  16. package/docs/commands.md +6 -0
  17. package/docs/components.md +73 -0
  18. package/docs/config-system.md +54 -0
  19. package/docs/cookies.md +6 -0
  20. package/docs/deployment.md +103 -13
  21. package/docs/devtools.md +5 -0
  22. package/docs/email-verification.md +26 -1
  23. package/docs/encryption.md +21 -0
  24. package/docs/errors.md +2 -0
  25. package/docs/flow/components.md +54 -0
  26. package/docs/flow/forms.md +57 -0
  27. package/docs/flow/references.md +14 -0
  28. package/docs/getting-started.md +38 -0
  29. package/docs/health.md +19 -0
  30. package/docs/helpers.md +99 -0
  31. package/docs/i18n.md +5 -0
  32. package/docs/inertia/props.md +70 -0
  33. package/docs/lock.md +15 -0
  34. package/docs/logger.md +38 -0
  35. package/docs/migrations.md +47 -0
  36. package/docs/monitor.md +59 -0
  37. package/docs/notifications.md +11 -0
  38. package/docs/orm/casts.md +6 -0
  39. package/docs/orm/lifecycle.md +18 -0
  40. package/docs/orm/queries.md +10 -0
  41. package/docs/orm/relationships.md +30 -0
  42. package/docs/queue.md +10 -0
  43. package/docs/rate-limiting.md +39 -4
  44. package/docs/responses.md +23 -0
  45. package/docs/routing.md +16 -0
  46. package/docs/scheduler.md +11 -0
  47. package/docs/session.md +6 -0
  48. package/docs/social.md +10 -0
  49. package/docs/storage.md +21 -0
  50. package/docs/support-policy.md +13 -1
  51. package/docs/telemetry.md +8 -0
  52. package/docs/tenancy.md +6 -0
  53. package/docs/testing/index.md +6 -0
  54. package/docs/validator.md +9 -0
  55. package/docs/view.md +6 -0
  56. package/package.json +3 -3
  57. package/src/install/ArchInstallCommand.ts +4 -2
  58. package/src/probe/topics.ts +40 -0
  59. package/src/provider/ArchProvider.ts +2 -2
@@ -177,6 +177,21 @@ When the model uses the ORM `SoftDeletes` mixin, the List page gains an
177
177
  **Active / All / Trashed** switch and the row + bulk actions gain Restore and
178
178
  Force-delete automatically — no extra configuration.
179
179
 
180
+ ## Types
181
+
182
+ | Type | What it is |
183
+ | -------------------------------------- | ------------------------------------------------------------------------ |
184
+ | `ActionItem` | One action as the panel holds it, after `action()` has built it. |
185
+ | `ActionHandler` | The callback an action runs, given its `ActionContext`. |
186
+ | `ActionContext` | What that callback receives — the record or selection, and the page. |
187
+ | `ActionVisible` | The predicate deciding whether an action appears for this record at all. |
188
+ | `ActionColor` | The emphasis it is drawn with. |
189
+ | `ConsoleAction`, `ConsoleHeaderAction` | The console's own row and header actions. |
190
+
191
+ **`ActionVisible` hides, it does not authorize.** An action absent from the UI is still
192
+ reachable by anyone who can form the request, so the authorization check belongs in the handler
193
+ as well — see [Authorization](/docs/authorization).
194
+
180
195
  ## Next steps
181
196
 
182
197
  - [Admin overview](/docs/admin) — the guide's front page and the rest of the sections.
@@ -83,6 +83,16 @@ Panel.configure({
83
83
  `adminTokensCss()` the `:root`/`.dark` custom properties — feed both into your own
84
84
  `tailwind.config` so a compiled stylesheet renders identically.
85
85
 
86
+ ## Types
87
+
88
+ | Type | What it is |
89
+ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
90
+ | `AdminAuthConfig` | Who may reach the panel and how they sign in. |
91
+ | `AdminAuthorizer` | The check run before a page renders. |
92
+ | `AuthRolesOptions` | How panel access maps onto the app's roles. |
93
+ | `AdminThemeConfig` | Panel theming. |
94
+ | `EnvironmentIndicatorOptions` | The banner naming which deployment you are looking at — worth setting, because the panel looks identical in staging and production and that is exactly when it matters. |
95
+
86
96
  ## Next steps
87
97
 
88
98
  - [Admin overview](/docs/admin) — the guide's front page and the rest of the sections.
@@ -149,6 +149,18 @@ Custom pages and anything contributed by a package sit in the same sidebar, sort
149
149
  and grouped by the same rules — a page's group heading is just a string, so a page
150
150
  and a resource that name the same group land together.
151
151
 
152
+ ## Types
153
+
154
+ | Type | What it is |
155
+ | ----------------------------------------- | --------------------------------------------------------------------------- |
156
+ | `DashboardWidget` | One widget as declared. |
157
+ | `WidgetContribution` | A widget pushed in by another package. |
158
+ | `WidgetTone` | Its good/warn/bad colouring. |
159
+ | `StatsResolver` | Supplies a stat widget's numbers for the selected range. |
160
+ | `ChartResolver`, `ChartData` | The same for a chart, and the shape it returns. |
161
+ | `TableWidgetColumn` | A column in a table widget — narrower than a resource table's. |
162
+ | `DashboardLayout`, `DashboardLayoutStore` | An arrangement of widgets, and where a user's own arrangement is persisted. |
163
+
152
164
  ## Next steps
153
165
 
154
166
  - [Admin overview](/docs/admin) — the guide's front page and the rest of the sections.
@@ -221,6 +221,20 @@ the table — the search term, each filter, the trashed scope — and each chip
221
221
  its own undo. A table showing four of two hundred rows for no visible reason is
222
222
  the most common way a panel misleads someone; the chips are the fix.
223
223
 
224
+ ## Types
225
+
226
+ The contribution shapes another package pushes into the panel:
227
+
228
+ | Type | What it is |
229
+ | ------------------------------------------------------------------ | ------------------------------------------------------------------------ |
230
+ | `NavItem`, `NavGroup`, `NavContribution` | A sidebar entry, a heading it sits under, and a contributed one. |
231
+ | `PageContribution` | A whole page added to the panel. |
232
+ | `UserMenu`, `UserMenuItem`, `UserMenuContribution` | The account menu and its entries. |
233
+ | `TopbarSlot` | Where something may be placed in the top bar. |
234
+ | `RenderHookName` | Every point the panel can be extended at — the enumeration of the slots. |
235
+ | `RenderHookContext` | What a hook receives when it runs. |
236
+ | `ConsoleContribution`, `ConsoleColumn`, `ConsoleRow`, `ConsoleTab` | Contributions to the console page. |
237
+
224
238
  ## Next steps
225
239
 
226
240
  - [Tables](/docs/admin/tables) — columns, filters and the query builder.
@@ -149,6 +149,21 @@ section("Line items").schema([
149
149
  ]),
150
150
  ```
151
151
 
152
+ ## Types
153
+
154
+ | Type | What it is |
155
+ | ------------------ | --------------------------------------------------------------------------------- |
156
+ | `FormComponent` | Anything that can appear in a form — a field, a layout block, a custom component. |
157
+ | `FormBlock` | A grouping: a section, a tab, a fieldset. |
158
+ | `FieldMode` | Whether a field is editable, read-only or hidden in this context. |
159
+ | `FieldPredicate` | The condition behind a conditional field. |
160
+ | `FormModeConfig` | How the form differs between create and edit. |
161
+ | `UploadedFileLike` | What a file field hands your handler. |
162
+
163
+ Infolists — the read-only counterpart — use `InfolistComponent`, with `EntryKind`,
164
+ `EntryDisplay`, `EntrySize` and `EntryWeight` describing one entry's type and presentation.
165
+ `PrimeKind` is the leading entry a record view opens with.
166
+
152
167
  ## Next steps
153
168
 
154
169
  - [Admin overview](/docs/admin) — the guide's front page and the rest of the sections.
@@ -217,6 +217,18 @@ A coloured strip across the top names the environment — red for production, am
217
217
  staging. It returns nothing in local development, so registering it unconditionally is
218
218
  the intended usage; there is nothing to switch off per environment.
219
219
 
220
+ ## Types
221
+
222
+ | Type | What it is |
223
+ | ------------------------------------------- | ------------------------------------------------------ |
224
+ | `HistoryOptions`, `HistoryChange` | Record history: what is tracked, and one entry of it. |
225
+ | `ImportRecordsPayload` | What the import action hands your handler. |
226
+ | `AdminNotification`, `StoredNotification` | A panel notification, and the persisted form. |
227
+ | `DatabaseNotificationOptions` | Where those are stored. |
228
+ | `StoreMediaOptions`, `DatabaseMediaOptions` | Media handling and its database backing. |
229
+ | `SearchHit`, `PanelSearchProvider` | One global-search result, and where results come from. |
230
+ | `CalloutTone` | The emphasis a callout is drawn with. |
231
+
220
232
  ## Next steps
221
233
 
222
234
  - [Panel Structure](/docs/admin/structure) — clusters, nested resources, multiple panels.
@@ -125,6 +125,12 @@ including on create.
125
125
  Both save hooks receive the `mode`, so one implementation can branch on `"create"`
126
126
  versus `"edit"` instead of duplicating the resource.
127
127
 
128
+ ## Types
129
+
130
+ `AdminModel` is what a resource's `model` accepts, and `AdminQuery` the query it builds — the
131
+ seam where the panel hands off to the ORM. `ListOptions` configures the list page,
132
+ and `RelationKind` names the relation types a relation manager understands.
133
+
128
134
  ## Next steps
129
135
 
130
136
  - [Admin overview](/docs/admin) — the guide's front page and the rest of the sections.
@@ -179,6 +179,27 @@ How the table renders — grid instead of rows, striping, sticky headers, densit
179
179
  and where the filters sit are covered in
180
180
  [Extending the UI](/docs/admin/extending-ui#table-presentation).
181
181
 
182
+ ## Types
183
+
184
+ What a column, filter and query rule are made of. All exported, so a helper that builds a
185
+ column set for several resources can be typed rather than repeated.
186
+
187
+ | Type | What it is |
188
+ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
189
+ | `ColumnKind` | Which column type this is — text, badge, boolean, date, and the rest. |
190
+ | `ColumnOption` | One column's settings. |
191
+ | `CellAlign` | `start` / `center` / `end`. |
192
+ | `ColumnSummary`, `SummaryKind`, `SummaryResult` | A footer aggregate: what to compute, and what comes back. |
193
+ | `BadgeTone` | The colouring a badge column maps a value to. |
194
+ | `EmptyState` | What the table shows instead of rows when there are none. |
195
+ | `PivotColumn` | A column reading a pivot table's own attributes on a many-to-many. |
196
+ | `FilterOption`, `FilterType`, `FilterApply` | A filter's declaration, its kind, and how it modifies the query. |
197
+ | `ConstraintKind`, `ConstraintOperator`, `ConstraintOption` | The query-builder constraints behind advanced filtering. |
198
+ | `QueryRule`, `Conjunction`, `QueryModifier` | One rule, how rules combine (`and` / `or`), and a raw modifier for what the builder cannot express. |
199
+ | `SavedView`, `SavedViewProvider` | A stored filter set, and where those are kept. |
200
+ | `TableRowsResolver` | Supplying rows yourself instead of letting the resource query. |
201
+ | `ExportFormat` | What the export action writes. |
202
+
182
203
  ## Next steps
183
204
 
184
205
  - [Admin overview](/docs/admin) — the guide's front page and the rest of the sections.
package/docs/audit.md CHANGED
@@ -359,6 +359,11 @@ app.container.singleton(
359
359
  | `auditLog` | `(event: AuditEvent, payload?: InstanceAuditPayload) => Promise<void>` | Log a custom event against this instance. |
360
360
  | `auditLogs` | `() => ModelQueryBuilder<AuditLog>` | Chainable query of this instance's history. |
361
361
 
362
+ ## Types
363
+
364
+ `AuditableOptions` is what `Model.using(Auditable)` accepts — which columns are tracked and
365
+ which are ignored. `AuditConfigShape` is the `audit` config namespace.
366
+
362
367
  ## Next steps
363
368
 
364
369
  - [ORM Lifecycle](/docs/orm/lifecycle) — the model hooks the audit system listens to.
@@ -94,7 +94,12 @@ export class User extends AuthUser {
94
94
  }
95
95
  ```
96
96
 
97
- `AuthUser` provides `getAuthId()` (returns `this.id`) and `getAuthPassword()` (returns `this.password`). Make `Auth.user()` return your concrete type by augmenting `UserModel` once:
97
+ `AuthUser` provides `getAuthId()` (returns `this.id`) and `getAuthPassword()` (returns `this.password`).
98
+
99
+ ### Bind the type — a required step
100
+
101
+ Do this once, or every `Auth.user()` in the app returns the framework's minimal `UserModel`
102
+ and reading your own columns off it is a type error:
98
103
 
99
104
  ```typescript fragment
100
105
  // bootstrap/app.ts (or any file imported at boot)
@@ -105,6 +110,17 @@ declare module "@zerotal/auth" {
105
110
  }
106
111
  ```
107
112
 
113
+ `AuthenticatableUser` in `@zerotal/core/contracts` is the same seam one layer down: `@zerotal/core`
114
+ has to talk about "the user" without depending on `@zerotal/auth`, so it declares an empty
115
+ interface for apps to augment, and `UserModel` extends it. Augmenting either resolves to the same
116
+ shape — augment `UserModel`, which is the one the auth docs and examples use.
117
+
118
+ **The empty body is the point.** The interface is not being given members — it is being
119
+ pointed at your class, so that everywhere the framework says `UserModel` it means `User`.
120
+ That is not a shape anyone guesses, which is why it is here in setup rather than filed
121
+ under advanced usage. Put it somewhere that is imported at boot and leave a comment saying
122
+ what it is for; it looks like dead code otherwise.
123
+
108
124
  ## How it works
109
125
 
110
126
  On every request `PersistUserMiddleware` reads `user_id` from the session and populates `ctx.user`. If `user_id` is present but the user no longer exists, the stale key is cleared.
@@ -133,6 +149,20 @@ await Auth.login(user); // write user_id to session, set ctx.user
133
149
  await Auth.logout(); // clear user_id from session, unset ctx.user
134
150
  ```
135
151
 
152
+ > **`Auth.user()` throws for a guest, and the name does not telegraph that.** It is the right
153
+ > call behind `AuthMiddleware`, where a user is guaranteed — that guarantee is what makes its
154
+ > non-optional return type honest. It is the wrong call on any route a signed-out person can
155
+ > reach. Plenty of audited things happen with nobody signed in: a storefront checkout, a
156
+ > customer approving a change from a tokenised link, an incoming webhook. On those routes an
157
+ > audit line that reaches for `Auth.user()` turns into a 401 on a public page, and the 401 is
158
+ > about the audit line rather than about the request.
159
+ >
160
+ > ```typescript fragment
161
+ > // A guest is a normal outcome here, not a failure.
162
+ > const actor = Auth.userOrNull();
163
+ > await Audit.record("checkout.completed", { actorId: actor?.getAuthId() ?? null });
164
+ > ```
165
+
136
166
  `Auth.attempt()` rolls credential lookup, password verification, and login into one call:
137
167
 
138
168
  ```typescript fragment
@@ -939,8 +969,87 @@ res.assertOk();
939
969
  res.assertSee("Welcome back");
940
970
  ```
941
971
 
972
+ ## Passkeys
973
+
974
+ A passkey replaces the password with a key pair held by the device: the private half never
975
+ leaves the authenticator, so there is nothing on your server worth stealing and nothing for a
976
+ user to reuse across sites. `PasskeyService` implements both WebAuthn ceremonies.
977
+
978
+ You supply where credentials are stored — the service does not assume a schema:
979
+
980
+ ```typescript fragment
981
+ import { PasskeyService } from "@zerotal/auth";
982
+
983
+ const passkeys = new PasskeyService({
984
+ rpName: "Acme",
985
+ rpId: "acme.test", // the domain, no scheme and no port
986
+ origin: "https://acme.test", // or an array, for staging alongside production
987
+ store: {
988
+ findUserCredentials: (userId) => Credential.where("user_id", userId).all(),
989
+ findCredential: (credentialId) => Credential.where("credential_id", credentialId).first(),
990
+ saveCredential: (credential) => Credential.create(credential),
991
+ updateCounter: (id, counter) => Credential.where("id", id).update({ counter }),
992
+ },
993
+ });
994
+ ```
995
+
996
+ Each ceremony is two calls — options out, response verified in. The challenge you hand back to
997
+ `verify…` is the one you issued, which is what stops a replay:
998
+
999
+ | Call | What it does |
1000
+ | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
1001
+ | `registrationOptions(user, existing?)` | Options for enrolment. Pass the user's existing credentials so the browser refuses to register the same authenticator twice. |
1002
+ | `verifyRegistration(user, response, challenge, ctx)` | Verifies enrolment and stores the credential. |
1003
+ | `authenticationOptions(userId?)` | Options for sign-in. Omit `userId` for a usernameless flow, where the authenticator offers the account. |
1004
+ | `verifyAuthentication(assertion, challenge, ctx)` | Returns `{ credential, userId }`, or the string `"passkey.invalid"` — a value rather than a throw, because a failed assertion is an ordinary outcome. |
1005
+
1006
+ `PasskeyUser` is the minimum the ceremonies need (`id`, `name`, `email`); `PasskeyCredential` is
1007
+ what your store holds.
1008
+
1009
+ > **`requireUserVerification` defaults to `true`, and that is what makes a passkey a second
1010
+ > factor.** With it off, an assertion succeeds on possession of an unlocked authenticator alone
1011
+ > — whoever picks up the phone is the user. Turn it off only for a deliberate "the device is the
1012
+ > credential" flow, and then do not count the passkey as a second factor.
1013
+
1014
+ `rpId` is the domain alone — no scheme, no port — and it must match the site the browser is on
1015
+ or the ceremony fails with an error that does not say why. `origin` takes an array when the same
1016
+ deployment answers on more than one.
1017
+
942
1018
  ## References
943
1019
 
1020
+ ### Types
1021
+
1022
+ Every middleware and broker takes an options shape, exported so a helper that builds one can be
1023
+ typed:
1024
+
1025
+ | Type | Configures |
1026
+ | ----------------------------------- | ----------------------------------------------------------------------- |
1027
+ | `AuthOptions` | `AuthMiddleware` — `redirectTo`, `mustVerifyEmail`, `verifyRedirectTo`. |
1028
+ | `GuestOptions` | `GuestMiddleware` — where a signed-in visitor is sent instead. |
1029
+ | `BasicAuthOptions` | HTTP Basic auth. |
1030
+ | `ConfirmPasswordOptions` | `ConfirmPasswordMiddleware`, including the confirmation window. |
1031
+ | `AuthenticateSessionOptions` | Session-guard behaviour on a password change elsewhere. |
1032
+ | `RememberMeOptions` | The persistent-login cookie. |
1033
+ | `TwoFactorOptions` | The second-factor gate. |
1034
+ | `LoginThrottleOptions` | Sign-in rate limiting. |
1035
+ | `EmailOtpOptions` | Passwordless codes by email. |
1036
+ | `MagicLinkBrokerOptions` | Magic-link sign-in. |
1037
+ | `PasskeysOptions` | `PasskeyService`, above. |
1038
+ | `CompromisedCheckOptions` | The have-i-been-pwned style check on a chosen password. |
1039
+ | `JwtGuardOptions`, `JwtSignOptions` | The JWT guard and how tokens are minted. |
1040
+ | `QrSvgOptions` | The 2FA enrolment QR code. |
1041
+
1042
+ What the brokers and guards hand back: `PasswordBrokerResult`, `MagicLinkBrokerResult` and
1043
+ `MagicLinkUser` for the link flows, `JwtPayload` and `TokenBundle` for JWT, and
1044
+ `PasskeyCredential` / `PasskeyUser` for passkeys.
1045
+
1046
+ ### Events
1047
+
1048
+ Emitted on `FrameworkEvents`, so an app can audit or react without wrapping the calls that
1049
+ raise them: `EmailVerified`, `PasswordConfirmed`, `PasswordResetLinkSent`, `PasswordResetEvent`
1050
+ and `CurrentDeviceLogout`. `LoginSucceeded`, `LoggedOut` and `OtherDeviceLogout` are documented
1051
+ above with the calls that emit them.
1052
+
944
1053
  ### Commands
945
1054
 
946
1055
  `@zerotal/auth` ships two commands:
@@ -77,6 +77,23 @@ Notifications can be delivered over a broadcast channel in real time — add `'b
77
77
  notification's `channels()` and implement `toBroadcast()`. See
78
78
  [Notifications → Broadcasting](/docs/notifications#broadcast).
79
79
 
80
+ ## Types
81
+
82
+ Channel and event types, most of which are inferred from your channel declarations:
83
+
84
+ | Type | What it is |
85
+ | --------------------------------------------------------- | --------------------------------------------------------------------------------------- |
86
+ | `StaticChannels`, `ParameterizedChannels` | Channels with fixed names, and those taking params. |
87
+ | `ChannelParams`, `ChannelParamRecord` | What a parameterised channel captures. |
88
+ | `ChannelAuthFn` | The authorization callback for a private channel. |
89
+ | `PresenceAuthFn`, `PresenceMemberData` | The same for presence, plus what a member publishes to the others. |
90
+ | `AuthorizeResult` | What an auth callback may return — a refusal, or the member data. |
91
+ | `TypedBroadcastEvent`, `EventsOf`, `PayloadOf` | An event on a channel, the events a channel carries, and one event's payload. |
92
+ | `BroadcastsModelEventsOptions`, `ModelBroadcastEventName` | Broadcasting a model's own lifecycle, and the event names it produces. |
93
+ | `BroadcastRecord` | A queued broadcast as stored. |
94
+ | `WsConnectionData` | What the server holds per connection. |
95
+ | `PusherPresenceResolver` | Resolving presence members when running against Pusher rather than the built-in server. |
96
+
80
97
  ## Next steps
81
98
 
82
99
  - [Broadcasting overview](/docs/broadcasting) — the guide's front page and the rest of the sections.
package/docs/cache.md CHANGED
@@ -381,6 +381,11 @@ does.
381
381
  > `CacheSerializationError` at write time. It is a bug in the caller rather than
382
382
  > a cache fault, so let it surface in development instead of swallowing it.
383
383
 
384
+ ## Types
385
+
386
+ `IdempotencyOptions` configures `Cache.idempotent()` — the key, the window, and what happens to
387
+ a second identical request that arrives while the first is still running.
388
+
384
389
  ## Next steps
385
390
 
386
391
  - [Query Builder](/docs/query-builder) — the queries you'll most often wrap in `remember()`.
package/docs/carbon.md CHANGED
@@ -823,6 +823,11 @@ test("survives a DST spring-forward", () => {
823
823
  | `forHumans` | `forHumans(options?: { join?: string; short?: boolean }): string` | Human-readable description. |
824
824
  | `toISO` | `toISO(): string` | ISO 8601 duration string (`toJSON` alias). |
825
825
 
826
+ ## Types
827
+
828
+ `DurationLike` is what the arithmetic methods accept — `add`, `subtract` and their kin — so a
829
+ duration can be built once and passed around rather than spelled out at each call site.
830
+
826
831
  ## Next steps
827
832
 
828
833
  - [Casts & Mutators](/docs/orm/casts) — `datetime` columns hydrate to Carbon automatically.
package/docs/changelog.md CHANGED
@@ -27,9 +27,167 @@ 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.9.0 — 2026-08-29
31
+
32
+ The gaps an app was filling in for itself: one Bun per project, a database backup that is not
33
+ `cp`, a release gate the pipeline will actually call, a boundary between a model and a page
34
+ prop, and helpers that work on both sides of the wire.
35
+
36
+ **Two things to know before upgrading.** Both are new refusals or new noise, and both are quiet
37
+ if they do not apply to you.
38
+
39
+ - **`zt` now refuses to run when a project has two Bun runtimes in it** — the shell's `bun` and
40
+ a different one in `node_modules`. If it fires, pick one: `bun update bun` moves the installed
41
+ copy to match your shell, or run everything through `node_modules/.bin/bun`. To boot anyway
42
+ while you sort it out, set `ZT_ALLOW_RUNTIME_MISMATCH=1`. Most projects never see this,
43
+ because most have no `bun` in `node_modules` to disagree with.
44
+ - **Passing an ORM model straight into an Inertia page prop now warns in development**, once per
45
+ model class, if that model declares neither `hidden` nor `visible`. Declaring either silences
46
+ it — and is the fix, not the silencer. Production is unaffected.
47
+
48
+ ### Added
49
+
50
+ - **One project, one Bun.** `engines.bun` is a floor and nothing enforced it, so an app could be
51
+ served by one runtime and tested on another — the shell's `bun` and a `node_modules/bun` put
52
+ there by a transitive peer dependency nobody declared. A green suite is then evidence about a
53
+ binary the app is not served by. `startZerotal()` refuses on a mismatch, and `zt test` spawns
54
+ the binary running it rather than a name `PATH` resolves.
55
+
56
+ - **`zt db:backup`** — a verified snapshot of the SQLite database, using `VACUUM INTO` rather
57
+ than `cp`. Copying a live SQLite file can capture a half-written page and produce a backup
58
+ that restores as a corrupt database, months later, from the one file you were relying on.
59
+ Every snapshot is opened and integrity-checked as it is written, `--require-rows` fails a
60
+ backup whose business tables are empty, `--rehearse` performs the actual restore, and every
61
+ failure path exits non-zero — a backup timer that reports success while writing nothing is
62
+ worse than no timer at all. See [Deployment](/docs/deployment#back-up-the-database).
63
+
64
+ - **`DeployTarget.preflight`** — a slot for the app's own release gate, run after the config
65
+ validators and `doctor` and before anything is built or migrated. A command named
66
+ `release:check` is found by convention, with nothing to wire up. A declared name that is not
67
+ registered **fails** the deploy rather than being skipped: a gate nothing calls is a comment.
68
+
69
+ - **`zerotal/shared`** — the helpers with no server in them, importable from a browser bundle:
70
+ `pluralize`, `Str`, and new `formatMoney` / `formatNumber` / `formatDate`. A total that reads
71
+ `R 39 147` on screen and `R39,147.00` on the invoice looks like two different numbers to the
72
+ person paying it, and maintaining that in two files is how it happens. See
73
+ [Helpers](/docs/helpers#sharing-helpers-with-the-browser--zerotalshared).
74
+
75
+ - **`<form data-enhance>`** — a plain server-rendered page, with no Flow component on it, can
76
+ submit without the page flashing. It posts through `fetch` and the matching form in the
77
+ response replaces it in place, so a validation error lands where the person is looking. Its
78
+ own dependency-free bundle at `/__flow/enhance.js`, added with `flowEnhanceTag()` in the
79
+ layout. Every path degrades: a network failure re-submits natively, a redirect is followed and
80
+ `pushState`d, and no JavaScript at all is an ordinary form post.
81
+
82
+ - **Three new `doctor` checks.** A rate limiter that cannot tell two people apart behind a proxy
83
+ — where the socket address is the proxy's for every request, so one attacker can lock out
84
+ everybody. Auth columns missing from a table a migration built without them, which otherwise
85
+ surfaces as `no such column` in tests that have nothing to do with email. And migrations that
86
+ have not run, named, before a request finds out.
87
+
88
+ ### Changed
89
+
90
+ - **A model reaching Inertia page props says what it is safe to publish.** Page props are page
91
+ source, and `return inertia("Trips/Show", { trip })` ships every column of the row — the
92
+ internal cost, the margin, the note about the customer, on the customer's own screen. The
93
+ ORM's `hidden` / `visible` lists were already honoured and nothing said so. See the upgrade
94
+ note above and [Inertia props](/docs/inertia/props#page-props-are-page-source).
95
+
96
+ - **A bound field the model will not accept says so.** `flow:model` on a column missing from
97
+ `fillable` was dropped in silence: the form submitted, nothing was written, nothing failed.
98
+ The drop stays — the same path receives whatever a browser sends — but a developer's typo no
99
+ longer produces the same silence as a hostile payload. Development only, once per field.
100
+
101
+ - **INTERNAL: 116 exports leave the recorded API surface** across `core`, `orm`, `flow`,
102
+ `admin`, `flow-ui` and `monitor`. **Nothing is removed and nothing breaks** — they are still
103
+ exported and still work; what changes is the promise. The dev orchestrator, the ORM's
104
+ connection wiring and dialect layer, the admin panel's page machinery and Flow's
105
+ wire-protocol frame types are not things an app constructs, and naming them in a `stable`
106
+ surface implied a guarantee about a protocol that is free to change. Each package's own
107
+ changelog lists its share.
108
+
109
+ - **A minor breaks nothing that can wait.** The roadmap used to say a minor never breaks
110
+ anything, which was false when written — three breaks had already shipped in minors, each
111
+ deliberately, each with a note, exactly as the [support policy](/docs/support-policy) has
112
+ always described. An absolute rule the project knowingly broke is worse than an honest one.
113
+
114
+ ### Documented
115
+
116
+ - **Every promised export is documented — 100%, up from 60%.** `maturity: stable` means an
117
+ export keeps its shape for the rest of the 1.x line, and the gate measuring how much of that
118
+ promise was written down stood at 798 gaps. It is zero.
119
+
120
+ Four features turned out to have shipped and been invisible. **Passkeys** — `PasskeyService`
121
+ has been here since 1.7.0 with no page at all, including that `requireUserVerification`
122
+ defaults to `true` because that is what makes a passkey a second factor rather than one.
123
+ **`@zerotal/core/env`**, a typed environment schema that reports every bad variable at once
124
+ rather than one per restart. **The outbound `Http` client**, which the testing guide had been
125
+ linking to a page that did not describe it. And **`@zerotal/monitor`'s Export JSON**, where the
126
+ button was documented and the forty-odd row types it hands you were not.
127
+
128
+ Also named for the first time: `@zerotal/flow-ui`'s sixty-one component prop types, which a
129
+ wrapper component cannot be written without.
130
+
131
+ The gate itself could not see `.tsx` files: with `jsx` unset, TypeScript declines to pull such
132
+ a module into the program rather than failing to parse it, so every symbol in one was
133
+ invisible. It had been inflating exactly the TSX-heavy packages.
134
+
135
+ ### Fixed
136
+
137
+ - **A rebuilt Inertia bundle no longer 404s on a chunk the browser asks for.**
138
+ `resources/js/app.tsx` builds to `/assets/app.js` under that name every time, while
139
+ `splitting: true` names each chunk after its content. A rebuild therefore rewrites `app.js` to
140
+ import `chunk-NEW.js` and prunes `chunk-OLD.js` — and a browser holding a cached `app.js` asks
141
+ for the pruned one:
142
+
143
+ GET /assets/chunk-hrnspqda.js status=404
144
+
145
+ from a page that renders and a server that is healthy, with nothing in that line leading back
146
+ to the template.
147
+
148
+ The template hardcodes `/assets/app.js` rather than calling `asset()`, so the version token the
149
+ rest of the framework appends never reached it — and cache-busting had only ever been
150
+ implemented for `serve --dev`. It now applies in every environment: the file's mtime in dev,
151
+ where a rebuild happens without a restart, and the boot-derived asset version otherwise. An
152
+ unchanged asset keeps a stable URL and stays cached, which is why the token is derived rather
153
+ than random.
154
+
155
+ ## 1.8.1 — 2026-08-26
156
+
157
+ DevTools showed you the wrong request, accurately.
158
+
159
+ ### Fixed
160
+
161
+ - **A page keeps the DevTools panel while its own assets load.** Opening `/login` selected
162
+ `/login`, then `/favicon.ico` a few milliseconds later, then `/css/app.css`. Live mode
163
+ selected every trace as it arrived and a page's sub-resources arrive right behind it, so the
164
+ bar named a request nobody asked about, the detail below described that request's headers
165
+ and its empty session, and the page you were inspecting had scrolled into the list. Nothing
166
+ shown was wrong; it was all about the wrong request.
167
+
168
+ Traces are now classified into three kinds rather than two, because "not the document" would
169
+ have suppressed the form post and the Inertia visit — the requests most worth watching. What
170
+ gets skipped over is narrower: a sub-resource the browser fetched on its own initiative. The
171
+ browser is asked rather than the URL, since an app may serve an API from a `.js` route and a
172
+ build that hashes its asset names has no extension to read; what was actually served is the
173
+ fallback, so a page fetched by `curl` still reads as a page. Anything unclassifiable counts
174
+ as app traffic, never as an asset — being wrong there decides whether a request is skipped,
175
+ and skipping the wrong one is how the panel stops showing what you came to see.
176
+
177
+ An asset still takes the selection when nothing else has it, so a panel opened mid-load
178
+ shows a request rather than an empty pane. A paused panel still counts assets toward its
179
+ pending badge.
180
+
181
+ ### Added
182
+
183
+ - **A `kind` facet on the DevTools All tab**, beside method and status. Assets were never the
184
+ problem, only their claim on the selection, so they are not hidden: pick `page` and `api`
185
+ for a list without fifty stylesheet fetches in it, or `asset` alone for what the browser
186
+ pulled in, what it cost and which of it 404'd — which was not visible anywhere before.
187
+
30
188
  ## 1.8.0 — 2026-08-24
31
189
 
32
- The first render mode, the codemod runner 2.0 depends on, and four failures that
190
+ The first render mode, the codemod runner 2.0 depends on, and five failures that
33
191
  each looked like something other than what they were.
34
192
 
35
193
  ### Added
@@ -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 |