@zerotal/core 1.8.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +204 -0
- package/api-surface.md +192 -424
- package/package.json +3 -1
- package/src/application/Application.ts +10 -4
- package/src/application/currentApp.ts +7 -1
- package/src/build/codemod.ts +1 -1
- package/src/command/CommandRunner.ts +2 -0
- package/src/command/OutputWriter.ts +5 -1
- package/src/command/builtin/AssetsPruneCommand.ts +138 -0
- package/src/command/builtin/DeployCommand.ts +91 -1
- package/src/command/builtin/TestCommand.ts +9 -2
- package/src/command/builtin/index.ts +1 -0
- package/src/command/startZerotal.ts +62 -1
- package/src/config/AppConfig.ts +10 -10
- package/src/config/ConfigLoader.ts +5 -1
- package/src/config/DeployConfig.ts +33 -0
- package/src/config/index.ts +6 -1
- package/src/config/registry.ts +5 -1
- package/src/config/validation.ts +10 -2
- package/src/conventions/ConventionLoader.ts +44 -3
- package/src/dev/BuildOutput.ts +58 -6
- package/src/dev/CssPlugins.ts +13 -3
- package/src/dev/DevBuildHook.ts +10 -2
- package/src/dev/DevDeck.ts +9 -3
- package/src/dev/DevOrchestrator.ts +2 -0
- package/src/dev/DevProcess.ts +19 -5
- package/src/dev/DevReloadMiddleware.ts +7 -1
- package/src/dev/DevSupervisor.ts +27 -9
- package/src/dev/bootBuild.ts +9 -1
- package/src/dev/index.ts +6 -1
- package/src/dev/reloadClient.ts +2 -0
- package/src/dev/startDevMode.ts +8 -2
- package/src/doctor/AppDoctor.ts +43 -1
- package/src/doctor/throttleIdentity.ts +118 -0
- package/src/errors/RuntimeMismatchError.ts +21 -0
- package/src/errors/index.ts +1 -0
- package/src/events/Emitter.ts +5 -1
- package/src/health/Health.ts +5 -5
- package/src/helpers/html.ts +2 -0
- package/src/helpers/index.ts +2 -1
- package/src/helpers/markdown.ts +14 -1
- package/src/helpers/pageElements.ts +2 -0
- package/src/http/HttpClient.ts +5 -1
- package/src/http/UploadedFile.ts +2 -2
- package/src/http/Uri.ts +5 -1
- package/src/http/negotiate.ts +5 -1
- package/src/http/originGuard.ts +2 -0
- package/src/http/sniffContentType.ts +7 -1
- package/src/index.ts +2 -1
- package/src/lock/LockManager.ts +4 -4
- package/src/logger/LoggerMiddleware.ts +1 -1
- package/src/logger/types.ts +4 -4
- package/src/macros/config.macro.ts +2 -0
- package/src/metrics/HttpMetrics.ts +10 -2
- package/src/middleware/BaseMiddleware.ts +20 -0
- package/src/middleware/CorsMiddleware.ts +6 -6
- package/src/middleware/RateLimiter.ts +105 -22
- package/src/middleware/SecureHeadersMiddleware.ts +8 -8
- package/src/middleware/ThrottleMiddleware.ts +57 -4
- package/src/middleware/WebhookMiddleware.ts +7 -7
- package/src/pipeline/ContextRegistry.ts +12 -2
- package/src/pipeline/HttpContext.ts +4 -0
- package/src/pipeline/currentPage.ts +4 -0
- package/src/pipeline/types.ts +2 -0
- package/src/router/FileRouter.ts +20 -2
- package/src/router/Route.ts +3 -0
- package/src/router/RouteHandler.ts +2 -2
- package/src/router/Router.ts +7 -5
- package/src/router/domain.ts +1 -0
- package/src/router/registry.ts +12 -2
- package/src/security/redactGraph.ts +1 -1
- package/src/shared/format.ts +135 -0
- package/src/shared/index.ts +34 -0
- package/src/storage/StorageFilesMiddleware.ts +4 -2
- package/src/storage/types.ts +10 -10
- package/src/support/cookie.ts +13 -7
- package/src/support/deepMerge.ts +49 -0
- package/src/support/runtime.ts +226 -0
- package/src/view/FileRouteResolver.ts +2 -0
- package/src/view/jsx-runtime.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,210 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.10.0] — 2026-08-30
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **Optional properties in public option shapes are declared `?: T | undefined`.**
|
|
16
|
+
The generated `tsconfig.json` turns on `exactOptionalPropertyTypes`, under which
|
|
17
|
+
`image?: string` and `image?: string | undefined` are different types: only the
|
|
18
|
+
second accepts a key that is present and holds `undefined`. So an app with the
|
|
19
|
+
strictness the framework shipped it could not write the most ordinary thing there
|
|
20
|
+
is — `{ image: candidate ?? undefined }` — and had to spell every conditional field
|
|
21
|
+
`...(x ? { x } : {})` instead. An options bag exists to be spread into; one built by
|
|
22
|
+
spreading has conditionally-absent keys.
|
|
23
|
+
|
|
24
|
+
438 properties across 115 files, applied by `scripts/exact-optional.ts` and checked
|
|
25
|
+
by `bun run exact:optional:check` so the convention holds for shapes written later.
|
|
26
|
+
Scoped to exported `*Options` / `*Config` shapes — what an app constructs — rather
|
|
27
|
+
than every optional property in the codebase. Nothing changes for a reader: an
|
|
28
|
+
absent optional property already read as `undefined`.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`zt assets:prune`** — remove the chunks an earlier release left behind, on the
|
|
33
|
+
machine that never ran a build. `assets:build --clean` cleans the directory it
|
|
34
|
+
_builds into_, which does nothing for the common release shape: build here, tar the
|
|
35
|
+
output, extract it over `public/` there. Extracting merges — nothing removes a file
|
|
36
|
+
the new release does not contain — so every deploy adds another set of
|
|
37
|
+
content-hashed chunks. One app reached 225 chunk files for the 49 its entry point
|
|
38
|
+
references. Ship `.zerotal/` with the release and this removes what the build record
|
|
39
|
+
does not claim; without a record it says so and removes nothing rather than guessing.
|
|
40
|
+
|
|
41
|
+
- **`definedOnly` (`@zerotal/core/helpers`) and the `Resolved<T>` type.** Widening the
|
|
42
|
+
option shapes moves a problem to the merge, and both of these are that problem's
|
|
43
|
+
answer. Object spread copies own properties even when their value is `undefined`,
|
|
44
|
+
so `{ ...DEFAULTS, ...options }` now lets an explicit `undefined` overwrite a
|
|
45
|
+
default; `definedOnly` drops those keys, turning "supplied as undefined" back into
|
|
46
|
+
"not supplied". (`deepMerge` already did this at every depth.)
|
|
47
|
+
|
|
48
|
+
`Resolved<T>` is what a defaults-applied object is. **`Required<T>` is not**, which
|
|
49
|
+
is the trap: `-?` removes the optionality a `?` introduced but not an `undefined`
|
|
50
|
+
written into the type, so `Required<Options>` quietly stopped meaning "the defaults
|
|
51
|
+
have been applied".
|
|
52
|
+
|
|
53
|
+
- **`zt deploy:<env> --check`** — the preflight gate on its own. A release script has a
|
|
54
|
+
moment where the new code is on disk and the service has not restarted yet, and that
|
|
55
|
+
is where a gate belongs: exit 0 and restart, exit non-zero and keep serving the
|
|
56
|
+
previous release. Everything that can refuse already runs by the end of preflight and
|
|
57
|
+
none of it mutates, so stopping there is a complete answer rather than half a deploy.
|
|
58
|
+
A workspace that has lost its banking details, or had its mail driver knocked back to
|
|
59
|
+
`log`, never goes live broken.
|
|
60
|
+
|
|
61
|
+
- **`engines.bun` is enforced.** Every generated app writes a floor and nothing had
|
|
62
|
+
ever read it. `runtimeMismatch` did not cover this: it compares the running Bun
|
|
63
|
+
against an _installed_ one, and most projects do not install Bun as a package, so
|
|
64
|
+
it correctly says nothing about most of them. `startZerotal()` now refuses to run
|
|
65
|
+
below the declared floor, so every `zt` command is covered.
|
|
66
|
+
|
|
67
|
+
The failure it catches is narrow and expensive. `Intl` output moves between Bun
|
|
68
|
+
releases, so a suite with currency or date assertions goes red on a runtime that is
|
|
69
|
+
otherwise fine, the failures name the code they touch, and the version is the last
|
|
70
|
+
thing anyone checks. `ZT_ALLOW_RUNTIME_MISMATCH=1` downgrades it to a warning.
|
|
71
|
+
|
|
72
|
+
- **`@zerotal/core/runtime`** (`zerotal/runtime`) — a subpath exporting the checks
|
|
73
|
+
themselves: `runtimeBelowFloor`, `declaredBunFloor`, `runtimeMismatch`, `bunBinary`
|
|
74
|
+
and the messages that go with them, so a script or a test can make the same
|
|
75
|
+
assertion. A subpath rather than the main barrel, which is deliberately lean.
|
|
76
|
+
|
|
77
|
+
- **A boot line when a convention is skipped in this environment.** An env-restricted
|
|
78
|
+
concern is skipped by _not looking_, which is correct and completely silent: from a
|
|
79
|
+
web process's point of view nothing is wrong, because from a web process's point of
|
|
80
|
+
view nothing exists. An app ran for weeks in production with `app/schedules` full
|
|
81
|
+
and no worker process — no inventory hold released, no payment reminder sent, and
|
|
82
|
+
nothing logged, because there was nothing to log. It was found by going looking.
|
|
83
|
+
|
|
84
|
+
`runConventions` now says so once, at boot, and only when the directory actually
|
|
85
|
+
holds files:
|
|
86
|
+
|
|
87
|
+
Skipping 3 file(s) in app/schedules — the "schedules" convention does not run in env=web (it runs in: worker, console).
|
|
88
|
+
|
|
89
|
+
"Skipping 0 schedules" on every boot of every app would be noise, and noise is how
|
|
90
|
+
the line that matters gets scrolled past.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- **Named rate limiters honour `trustedProxies`.** `ThrottleMiddleware` resolves the
|
|
95
|
+
client address from the socket unless told how many proxies sit in front — that was
|
|
96
|
+
fixed, documented and covered by a doctor check. `RateLimiter`'s `.byIp()`,
|
|
97
|
+
`.byUser()` and `.byApiKey()` did not: they used a private resolver that read the
|
|
98
|
+
socket address and fell back to the **leftmost** `X-Forwarded-For` entry, with no
|
|
99
|
+
proxy count anywhere in it. A `keyResolver` replaces the middleware's own
|
|
100
|
+
proxy-aware path entirely, so the sibling API kept the bug the middleware had shed.
|
|
101
|
+
|
|
102
|
+
Two consequences, in opposite directions. Where there was no socket address a client
|
|
103
|
+
picked its own bucket by writing the header, which is the part of it the client
|
|
104
|
+
controls. Behind a real reverse proxy — where there _is_ a socket address — it was
|
|
105
|
+
the proxy's, so **every visitor shared one bucket**: the documented
|
|
106
|
+
`RateLimiter.for("login").limit(5).every(60)` was five attempts a minute across the
|
|
107
|
+
entire user base, and one attacker locked everybody out.
|
|
108
|
+
|
|
109
|
+
All three now resolve through the same proxy-aware function, and
|
|
110
|
+
`.trustedProxies(n)` is on the builder. The strategy is recorded and the resolver
|
|
111
|
+
built at the end, so `.byIp().trustedProxies(1)` and `.trustedProxies(1).byIp()`
|
|
112
|
+
mean the same thing.
|
|
113
|
+
|
|
114
|
+
- **`zt doctor` audits named limiters.** Its `trustedProxies` check exempted any
|
|
115
|
+
throttle with a custom `keyResolver` — right for one keyed on a user id, where no
|
|
116
|
+
proxy can affect the answer, and exactly wrong for `RateLimiter`'s built-ins, which
|
|
117
|
+
_are_ custom resolvers and do key on an address. The check that exists to catch this
|
|
118
|
+
skipped the whole API that still had it. The built-in resolvers mark themselves now;
|
|
119
|
+
an app's own `.by(fn)` stays exempt, which is the point of the exemption.
|
|
120
|
+
|
|
121
|
+
- **The build record is portable.** Its filename was hashed from the output
|
|
122
|
+
directory's _absolute_ path, so the same project built at `/home/me/app` and
|
|
123
|
+
unpacked at `/opt/app` wrote and looked for two different files. A record shipped
|
|
124
|
+
with a release matched nothing at the other end — which is why a server could not
|
|
125
|
+
say which files belonged to the current release — and moving a checkout silently
|
|
126
|
+
orphaned it. It is keyed on the path relative to the project now.
|
|
127
|
+
|
|
128
|
+
## [1.9.0] — 2026-08-29
|
|
129
|
+
|
|
130
|
+
### Added
|
|
131
|
+
|
|
132
|
+
- **`@zerotal/core/env` is documented.** A whole subpath — a strict, fully typed environment
|
|
133
|
+
schema — had no page. `EnvSchema.define()` with the `t` field builders reports **every**
|
|
134
|
+
failing variable at once rather than one per restart, which is the difference between one fix
|
|
135
|
+
and three round trips through a deploy. See
|
|
136
|
+
[Configuration](/docs/config-system#declaring-the-whole-environment--envschema).
|
|
137
|
+
|
|
138
|
+
- **`negotiate()` is documented.** One route, three audiences: a browser, an API client and the
|
|
139
|
+
console, without three copies of the logic. It is what the framework's own error handler uses,
|
|
140
|
+
which is why a 422 is a redirect-with-errors for a form post and a JSON body for a fetch.
|
|
141
|
+
|
|
142
|
+
- **The hashing helpers get the paragraph `safeEqual` deserves.** `a === b` on a token returns as
|
|
143
|
+
soon as two bytes differ, and how long it took measures how much of the prefix was right —
|
|
144
|
+
enough, over many attempts, to recover a secret a character at a time.
|
|
145
|
+
|
|
146
|
+
- Routing, config, cookie, view, storage and Carbon types are named.
|
|
147
|
+
|
|
148
|
+
### Changed
|
|
149
|
+
|
|
150
|
+
- **INTERNAL: 58 exports are marked `@internal`** — the file-routing internals, the router's
|
|
151
|
+
compiled state, the context registry, the metrics instrumentation hooks, the view component
|
|
152
|
+
symbols, and the config map/path types. Still exported, still working; none is something an
|
|
153
|
+
app constructs.
|
|
154
|
+
|
|
155
|
+
### Changed
|
|
156
|
+
|
|
157
|
+
- **INTERNAL: the dev orchestrator's 26 exports are marked `@internal`.** `DevSupervisor`,
|
|
158
|
+
`DevChild`, `DevSpawnFn`, `StreamDeck`, `TabsDeck`, `createDeck`, `collectDevProcesses`,
|
|
159
|
+
`buildCssBundle`, `buildJsBundle`, `bootBuildDecision`, `detectCssPlugins`, `isWritableDir`,
|
|
160
|
+
`startDevMode`, `registerDevHtmlSnippet`, `DEV_RELOAD_CLIENT`, `SERVER_PROCESS_NAME` and their
|
|
161
|
+
option/result types.
|
|
162
|
+
|
|
163
|
+
**Nothing is removed and nothing breaks** — they are still exported and still work. What
|
|
164
|
+
changes is the promise: they leave the recorded API surface, because an app never constructs
|
|
165
|
+
any of them and the only callers outside `@zerotal/core` are sibling framework packages wiring
|
|
166
|
+
their own dev-time build. `DevProcessDefinition` — the one a package author actually writes —
|
|
167
|
+
is unaffected and stays documented.
|
|
168
|
+
|
|
169
|
+
### Added
|
|
170
|
+
|
|
171
|
+
- **One project, one Bun.** `engines.bun` is a floor and nothing enforces it, so a project
|
|
172
|
+
can end up serving its app with one runtime and running its suite with another — the
|
|
173
|
+
shell's `bun` and a `node_modules/bun` put there by a transitive peer dependency nobody
|
|
174
|
+
declared. Nothing announces that, and a green suite is then evidence about a binary the app
|
|
175
|
+
is not served by. `startZerotal()` now compares `Bun.version` against the installed
|
|
176
|
+
manifest and refuses on a mismatch, naming both versions and the two ways out. It is not a
|
|
177
|
+
pin: the version to agree on is whichever one the project installed, so `bun update bun`
|
|
178
|
+
moves it. `ZT_ALLOW_RUNTIME_MISMATCH=1` downgrades the refusal to a warning.
|
|
179
|
+
|
|
180
|
+
- **`DeployTarget.preflight`** — a slot for the app's own release gate, run after the config
|
|
181
|
+
validators and `doctor` and before anything is built or migrated. A command named
|
|
182
|
+
`release:check` is found by convention with nothing to wire up. A declared name that is not
|
|
183
|
+
registered **fails** the deploy rather than being skipped: a missing `inertia:build` means
|
|
184
|
+
the app has no Inertia, but a missing gate means the gate is not running, which is the
|
|
185
|
+
state the feature exists to prevent.
|
|
186
|
+
|
|
187
|
+
- **`zerotal/shared`** — the helpers that are safe to import from a browser bundle:
|
|
188
|
+
`pluralize`, `singularize`, `snakeCase`, `camelCase`, `tableNameFor`, `Str`, and new
|
|
189
|
+
`formatMoney` / `formatNumber` / `formatDate`. Everything reachable from it is pure, and a
|
|
190
|
+
test bundles the entry point for the browser to keep it that way. Without it a page that
|
|
191
|
+
wants the framework's `pluralize` gets a second implementation written by hand, and the
|
|
192
|
+
second copy is always the worse one — `"supplier line"` pluralises to `"supplier lines"`,
|
|
193
|
+
and the naive rule gives `"suppliers line"`.
|
|
194
|
+
|
|
195
|
+
- **`doctor` check: rate-limit identity.** `ThrottleMiddleware` keys on the socket address
|
|
196
|
+
unless told how many proxies sit in front of the app. That default is right and it is the
|
|
197
|
+
wrong answer the moment you deploy behind one: the socket address is then the proxy's for
|
|
198
|
+
every request, everybody shares a bucket, and the limiter inverts into a way for one
|
|
199
|
+
attacker to lock out the rest. Nothing observable says so. A production-like deployment
|
|
200
|
+
with a registered throttle and no `trustedProxies` is now reported.
|
|
201
|
+
|
|
202
|
+
### Changed
|
|
203
|
+
|
|
204
|
+
- **`zt test` spawns the binary running it**, not the name `bun` for the OS to resolve
|
|
205
|
+
against `PATH`. A command whose job is to run this app's tests was handing them to whatever
|
|
206
|
+
the shell happened to offer, so `node_modules/.bin/bun zt test` satisfied every check in
|
|
207
|
+
the parent process and still ran the suite on a different runtime.
|
|
208
|
+
|
|
209
|
+
- **`BaseMiddleware.with()`** now documents that each call creates a distinct class and any
|
|
210
|
+
per-class state goes with it — so re-using one `.with()` export on two routes shares that
|
|
211
|
+
state. For `ThrottleMiddleware` that state is the hit counter, and on a sign-in flow it
|
|
212
|
+
means a handful of fumbled passwords can spend the allowance a legitimate person needs to
|
|
213
|
+
answer their second factor.
|
|
214
|
+
|
|
11
215
|
## [1.8.0] — 2026-08-24
|
|
12
216
|
|
|
13
217
|
### Added
|