@shaferllc/keel 0.81.1 → 0.81.2
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/docs/ai-manifest.json +8 -1
- package/docs/changelog.md +2095 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2095 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Keel are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to
|
|
5
|
+
adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.81.1] — 2026-07-12
|
|
8
|
+
|
|
9
|
+
### Documentation
|
|
10
|
+
|
|
11
|
+
- **Full API-reference entries for the new ORM surface.** `0.81.0` shipped the
|
|
12
|
+
guides; this fills in the per-method reference the docs maintain for everything
|
|
13
|
+
else — the query-builder additions (`join`/`leftJoin`, `groupBy`/`having`/
|
|
14
|
+
`distinct`, `whereColumn`/`whereRaw`/`orderByRaw`, `when`, `increment`/
|
|
15
|
+
`decrement`, `upsert`/`insertOrIgnore`, `chunk`), the migration builders
|
|
16
|
+
(`index`/`foreign`/`alterTable`, `AlterTableBuilder`, `ForeignKeyBuilder`), and
|
|
17
|
+
the model additions (`with`/`withCount`/`whereHas`/`has`/`doesntHave`,
|
|
18
|
+
`ModelQuery`, lifecycle events + `observe`, global scopes, soft deletes,
|
|
19
|
+
`hidden`/`visible`/`appends`, `morphMany`/`morphOne`/`morphTo`/
|
|
20
|
+
`registerMorphType`). Corrects two now-stale notes ("no soft-delete built in",
|
|
21
|
+
"nested eager loading isn't here yet"). `llms.txt` / `llms-full.txt` /
|
|
22
|
+
`ai-manifest.json` regenerated to match.
|
|
23
|
+
|
|
24
|
+
## [0.81.0] — 2026-07-12
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **ORM: Eloquent-parity features.** The active-record `Model` grows most of the
|
|
29
|
+
Eloquent surface people reach for, all backward-compatible and still on the
|
|
30
|
+
driver-agnostic query builder (no JOINs, edge-safe):
|
|
31
|
+
|
|
32
|
+
- **Lifecycle events & observers** — `creating`/`created`, `updating`/
|
|
33
|
+
`updated`, `saving`/`saved`, `deleting`/`deleted`, `restoring`/`restored`,
|
|
34
|
+
`retrieved`. The `*ing` events are **cancelable** (a hook returning `false`
|
|
35
|
+
vetoes the write); `Model.observe({...})` attaches an observer object.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
User.creating((u) => { u.uuid = crypto.randomUUID(); });
|
|
39
|
+
User.deleting((u) => (u.isRoot ? false : undefined)); // veto
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **Global scopes** (`addGlobalScope`) applied to every query the model builds
|
|
43
|
+
and **inherited by subclasses** — the base for tenancy and published-only
|
|
44
|
+
reads. `withoutGlobalScope(...)` opts out, explicitly and greppably. Local
|
|
45
|
+
scopes are just static methods returning `query()`.
|
|
46
|
+
|
|
47
|
+
- **Soft deletes** — `static softDeletes = true` + `deleted_at`; `delete()`
|
|
48
|
+
sets the timestamp, a scope hides trashed rows, and `withTrashed`/
|
|
49
|
+
`onlyTrashed`/`restore`/`forceDelete`/`trashed` round it out.
|
|
50
|
+
|
|
51
|
+
- **`with` / `withCount` / `whereHas` / `has` / `doesntHave`** — a model-aware
|
|
52
|
+
`ModelQuery` with **nested** eager loading (`"posts.comments"`) and
|
|
53
|
+
relationship-existence filters, via a two-query strategy (no JOIN).
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
await User.query().with("posts.comments").withCount("posts")
|
|
57
|
+
.whereHas("posts", (q) => q.where("published", true)).get();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- **Serialization control** — `static hidden` / `visible` / `appends` on
|
|
61
|
+
`toJSON()` (appends resolve getters or zero-arg methods).
|
|
62
|
+
|
|
63
|
+
- **Polymorphic relations** — `morphOne` / `morphMany` / `morphTo` with a
|
|
64
|
+
morph-type registry (`registerMorphType`), eager loading across mixed types,
|
|
65
|
+
and `whereHas`/`withCount` support.
|
|
66
|
+
|
|
67
|
+
- **Query builder** grows `join`/`leftJoin`, `groupBy`/`having`, `distinct`,
|
|
68
|
+
`whereColumn`, `whereRaw`, `orderByRaw`, `when()`, `increment`/`decrement`,
|
|
69
|
+
dialect-aware `upsert`/`insertOrIgnore`, and `chunk()` for paged iteration over
|
|
70
|
+
large tables.
|
|
71
|
+
|
|
72
|
+
- **Migrations** grow `index()`/`uniqueIndex()` and `foreign().references().on()`
|
|
73
|
+
in `createTable`, plus **`SchemaBuilder.alterTable`** (add/drop/rename column,
|
|
74
|
+
add/drop index) — so altering a table no longer needs hand-written `raw()` SQL.
|
|
75
|
+
|
|
76
|
+
- **Accounts** — a new `@shaferllc/keel/accounts` package: password reset, email
|
|
77
|
+
verification, and two-factor auth (TOTP + single-use recovery codes), driven by
|
|
78
|
+
`attempt()` and an `AccountsServiceProvider`, with its own migration and
|
|
79
|
+
publishable config.
|
|
80
|
+
|
|
81
|
+
- **Teams** — a new `@shaferllc/keel/teams` package: multi-tenancy with a
|
|
82
|
+
`TenantModel` (a `TENANT_SCOPE` global scope), request-scoped team context, and
|
|
83
|
+
invitations.
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- `Model.create()` now routes through `save()`, so mass-assignment, timestamps,
|
|
88
|
+
and the `saving`/`creating` events all apply in one place.
|
|
89
|
+
|
|
90
|
+
## [0.80.0] — 2026-07-12
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
|
|
94
|
+
- **Billing** — a new `@shaferllc/keel/billing` package: a Cashier-style
|
|
95
|
+
subscription layer with **one gateway-neutral API over Stripe and Paddle**
|
|
96
|
+
(switching gateways is a config change). `class User extends Billable(Model)`
|
|
97
|
+
gives a gateway customer, subscriptions (create/swap/quantity/trials/cancel/
|
|
98
|
+
resume + status checks), single charges + refunds, invoices, hosted checkout,
|
|
99
|
+
and verified per-gateway webhooks that sync local state and emit typed events.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
class User extends Billable(Model) { static table = "users"; }
|
|
103
|
+
await user.newSubscription("default", "price_pro").trialDays(14).create(pmId);
|
|
104
|
+
if (await user.subscribed()) { /* … */ }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Reaches the active gateway from model methods through a module-level singleton
|
|
108
|
+
(`setBilling`/`billing`), matching Keel's `setConnection`/`setLogger` pattern.
|
|
109
|
+
Ships `Subscription`/`SubscriptionItem` models, a gateway-neutral migration, a
|
|
110
|
+
publishable config stub, and an in-memory `FakeGateway` so billing flows test
|
|
111
|
+
without a network. Webhook signatures are verified with a vendored hex
|
|
112
|
+
HMAC-SHA256 (edge-safe Web Crypto). Paddle's merchant-of-record differences
|
|
113
|
+
(checkout-created subscriptions, no raw card handling) surface as clear
|
|
114
|
+
`BillingError`s rather than silent gaps.
|
|
115
|
+
|
|
116
|
+
## [0.79.0] — 2026-07-12
|
|
117
|
+
|
|
118
|
+
### Added
|
|
119
|
+
|
|
120
|
+
- **Route model binding.** A `:post` in the path arrives as a **`Post`**, not a
|
|
121
|
+
string.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
bindModel("post", Post);
|
|
125
|
+
|
|
126
|
+
router.get("/posts/:post", (c) => {
|
|
127
|
+
const post = boundModel(Post); // already fetched. Not a string, not null.
|
|
128
|
+
return c.json(post);
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The row is looked up **before the handler runs**, and a miss is a 404 there and
|
|
133
|
+
then — so the handler never sees a `null` and never has to remember to check for
|
|
134
|
+
one. **"Forgot the 404" stops being a bug you can write.**
|
|
135
|
+
|
|
136
|
+
- **`key`** binds by another column (`/posts/hello-world` → `{ key: "slug" }`).
|
|
137
|
+
- **`scope` is security, not a filter.** A row outside it is a **404** — not a 403
|
|
138
|
+
(which would confirm it exists), and not merely absent from a list — so it can't
|
|
139
|
+
be reached by *guessing its id*. The scope gets the request, so it can depend on
|
|
140
|
+
who's asking. Tested against a real database, asserting the handler never runs
|
|
141
|
+
for an out-of-scope row.
|
|
142
|
+
- **Binding runs before route middleware**, so a policy can read the model instead
|
|
143
|
+
of re-fetching it.
|
|
144
|
+
- **`bindRoute(param, fn)`** resolves anything that isn't a model — a tenant, a
|
|
145
|
+
feature flag; returning `undefined` is a 404.
|
|
146
|
+
- An unbound param is untouched (still a string), and only routes with parameters
|
|
147
|
+
pay for any of this. Two params bound to the same model must be disambiguated —
|
|
148
|
+
guessing would be worse than asking.
|
|
149
|
+
|
|
150
|
+
## [0.78.2] — 2026-07-11
|
|
151
|
+
|
|
152
|
+
### Added
|
|
153
|
+
|
|
154
|
+
- **Releases publish themselves.** Pushing a `v*` tag now builds, tests, and
|
|
155
|
+
publishes to npm from CI, via **trusted publishing** (OIDC) — no token to leak or
|
|
156
|
+
rotate, and npm attaches a **provenance attestation**: proof the tarball was built
|
|
157
|
+
from that commit by that workflow, rather than uploaded from a laptop.
|
|
158
|
+
|
|
159
|
+
Publishing by hand is why npm drifted so far from git — git reached v0.77.0 while
|
|
160
|
+
npm's `latest` still said 0.74.0, and the published versions were sporadic (0.12,
|
|
161
|
+
0.35, 0.58, 0.66, 0.68, 0.74). The tag is the release now.
|
|
162
|
+
|
|
163
|
+
It refuses to publish if the tag disagrees with `package.json`, or if the
|
|
164
|
+
typecheck, tests, or build fail — a tag can point at any commit, including one CI
|
|
165
|
+
never saw.
|
|
166
|
+
|
|
167
|
+
(v0.78.1 was tagged before this workflow existed, so it never reached npm; its
|
|
168
|
+
`./package.json` export fix ships here.)
|
|
169
|
+
|
|
170
|
+
## [0.78.1] — 2026-07-11
|
|
171
|
+
|
|
172
|
+
### Fixed
|
|
173
|
+
|
|
174
|
+
- **`./package.json` is exported.** With an `exports` map, Node blocks
|
|
175
|
+
`require("@shaferllc/keel/package.json")` unless it's listed — and plenty of
|
|
176
|
+
tooling reads it (bundlers, version checks, framework plugins). Caught by
|
|
177
|
+
installing the published package and poking at it, which is the only way to see
|
|
178
|
+
this class of problem.
|
|
179
|
+
|
|
180
|
+
## [0.78.0] — 2026-07-11
|
|
181
|
+
|
|
182
|
+
### Removed
|
|
183
|
+
|
|
184
|
+
- **The demo app is gone from the framework repo.** `app/`, `bootstrap/`, `config/`,
|
|
185
|
+
`routes/`, `resources/`, `database/`, `bin/keel.ts` and `vite.config.ts` were an
|
|
186
|
+
example application living inside the library. None of it shipped (the `files`
|
|
187
|
+
field is `dist`, `docs`, and the MCP bin), and after 0.76.0 inverted the CLI's
|
|
188
|
+
dependency, **nothing in the framework referenced it any more**.
|
|
189
|
+
|
|
190
|
+
This repo is now the framework, and only the framework. Application code —
|
|
191
|
+
controllers, providers, routes, config — belongs in *your* repo; the
|
|
192
|
+
[starter app](https://github.com/shaferllc/keel-app) has the layout.
|
|
193
|
+
|
|
194
|
+
The `keel` / `serve` / `dev` / `dev:client` / `build:client` npm scripts went with
|
|
195
|
+
it: they existed to run the demo. `npm test`, `npm run typecheck`, `npm run build`
|
|
196
|
+
and `npm run verify:release` are what you run here.
|
|
197
|
+
|
|
198
|
+
## [0.77.0] — 2026-07-11
|
|
199
|
+
|
|
200
|
+
### Changed
|
|
201
|
+
|
|
202
|
+
- **The console runs on Keel's own console.** All 20 built-in commands (`serve`,
|
|
203
|
+
`routes`, `repl`, `mcp`, every `make:*`, `migrate:*`, `vendor:publish`) are now
|
|
204
|
+
`defineCommand()`s on the `ConsoleKernel`. **`commander` is gone** — not moved to
|
|
205
|
+
a runtime dependency, removed.
|
|
206
|
+
|
|
207
|
+
This was forced by shipping the console in 0.76.0: `@shaferllc/keel/cli` imported
|
|
208
|
+
`commander`, which was a *devDependency*, so a consumer installing the package got
|
|
209
|
+
`ERR_MODULE_NOT_FOUND` on import. Promoting commander to a runtime dep would have
|
|
210
|
+
fixed the symptom while shipping a second command system alongside the one we'd
|
|
211
|
+
just built. So the built-ins moved instead.
|
|
212
|
+
|
|
213
|
+
What you get for it: **generated help** (`keel help make:controller` prints usage,
|
|
214
|
+
args, and options), commands **grouped by namespace**, `routes` and `migrate:status`
|
|
215
|
+
as real tables, and typed flags everywhere — `keel routes --nope` is now an error
|
|
216
|
+
rather than a shrug.
|
|
217
|
+
|
|
218
|
+
- **`PackageCommand` is now `defineCommand()`'s shape** — it was typed against a
|
|
219
|
+
commander `Command`. A package's command gets typed args and flags, generated help,
|
|
220
|
+
the terminal UI, and the prompt-trapping test story for free.
|
|
221
|
+
|
|
222
|
+
**Breaking** for a package contributing commands: replace `configure`/`action` with
|
|
223
|
+
`flags`/`args`/`run`. Both in-tree packages (`openapi:export`, `watch:prune`) are
|
|
224
|
+
ported.
|
|
225
|
+
|
|
226
|
+
That port fixed a live bug: `watch:prune --hours lots` did `Number("lots")` → `NaN`,
|
|
227
|
+
so the retention cutoff became `NaN` and the prune silently misbehaved. A typed
|
|
228
|
+
`flag.number()` rejects it as a usage error.
|
|
229
|
+
|
|
230
|
+
- **`@hono/node-server` is an optional peer dependency**, dynamically imported by
|
|
231
|
+
`serve`. It's only needed to *serve* on Node — a Workers app has no reason to
|
|
232
|
+
install it — and a missing one now says so instead of failing at import.
|
|
233
|
+
|
|
234
|
+
## [0.76.0] — 2026-07-11
|
|
235
|
+
|
|
236
|
+
### Added
|
|
237
|
+
|
|
238
|
+
- **The console ships in the package.** `@shaferllc/keel/cli` — so an app gets
|
|
239
|
+
`serve`, `routes`, `repl`, `migrate:*`, and every `make:*` generator from the
|
|
240
|
+
dependency, rather than having to vendor them.
|
|
241
|
+
|
|
242
|
+
### Changed
|
|
243
|
+
|
|
244
|
+
- **`run(argv, { createApplication })` — the console is handed an application
|
|
245
|
+
factory instead of importing one.** `src/core/cli/index.ts` imported
|
|
246
|
+
`bootstrap/app.ts`: the *framework* depended on an *application*, which is the
|
|
247
|
+
dependency pointing the wrong way. It also had consequences —
|
|
248
|
+
- the file reached outside `rootDir: src`, so `tsconfig.build.json` had to
|
|
249
|
+
**exclude it from the build**;
|
|
250
|
+
- which meant the console was **not in the published package at all** (only the
|
|
251
|
+
`keel-mcp` bin was);
|
|
252
|
+
- and it's why `runCommand()` in the testing toolkit takes a callback rather than
|
|
253
|
+
an argv array — importing the CLI from `testing.ts` broke the build.
|
|
254
|
+
|
|
255
|
+
Your `bin/keel.ts` now passes its own factory. The CLI compiles, ships, and is
|
|
256
|
+
importable.
|
|
257
|
+
|
|
258
|
+
**Breaking** for anyone calling `run()` directly: it takes a second argument.
|
|
259
|
+
A one-line change in `bin/keel.ts`.
|
|
260
|
+
|
|
261
|
+
## [Unreleased]
|
|
262
|
+
|
|
263
|
+
### Added
|
|
264
|
+
|
|
265
|
+
- **CI.** Every push and pull request now runs the checks that were, until now, only
|
|
266
|
+
ever run by hand in the right order by someone who remembered to:
|
|
267
|
+
- `typecheck` — **src and tests**. The suite went unchecked for a long time, so a
|
|
268
|
+
test asserting a type at compile time was asserting nothing.
|
|
269
|
+
- `test`.
|
|
270
|
+
- **`build`** — the check that matters most. A git install runs the build through
|
|
271
|
+
`prepare`, so a tree that doesn't build cannot be installed *at all* — and
|
|
272
|
+
neither the tests nor the typecheck can see it, because they run against the
|
|
273
|
+
working directory. CI's checkout **is** the committed tree, which is exactly
|
|
274
|
+
what shipped three uninstallable tags (v0.74.0–v0.74.2).
|
|
275
|
+
- `typecheck:docs` — the docs examples compile against `dist/`, i.e. the real
|
|
276
|
+
published surface. They have caught bugs the tests could not: inference that
|
|
277
|
+
worked in-repo but broke for a consumer.
|
|
278
|
+
- **The generated AI surface is in sync** — regenerating `llms.txt`,
|
|
279
|
+
`llms-full.txt` and `docs/ai-manifest.json` must be a no-op, or someone changed
|
|
280
|
+
a doc or an export and shipped a stale surface.
|
|
281
|
+
- **No control characters in source** — we have shipped both a NUL byte (a cache
|
|
282
|
+
key) and raw ANSI escapes (the console colors), each of which turns a text file
|
|
283
|
+
"binary" to grep, diff, and code review.
|
|
284
|
+
|
|
285
|
+
## [0.75.1] — 2026-07-11
|
|
286
|
+
|
|
287
|
+
### Changed
|
|
288
|
+
|
|
289
|
+
- **Removed the Remult references** from the API-resources guide and source
|
|
290
|
+
comments. Keel isn't that, and the docs shouldn't read as a comparison to another
|
|
291
|
+
framework. The surrounding sentences are rewritten so they still say what the
|
|
292
|
+
feature does rather than leaving a hole. No behavior change.
|
|
293
|
+
|
|
294
|
+
## [0.75.0] — 2026-07-11
|
|
295
|
+
|
|
296
|
+
### Added
|
|
297
|
+
|
|
298
|
+
- **API resources — a full CRUD REST API from a model.** `@shaferllc/keel/api`.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
apiResource(router, Post, {
|
|
302
|
+
filter: ["status", "authorId"],
|
|
303
|
+
sort: ["createdAt", "title"],
|
|
304
|
+
body: PostSchema,
|
|
305
|
+
access: { read: true, write: (c) => isEditor(c) },
|
|
306
|
+
scope: (q, c) => q.where("authorId", currentUserId(c)),
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
It registers **real routes** on the router — so `url()` finds them, `keel routes`
|
|
311
|
+
lists them, and `@shaferllc/keel/openapi` documents them for free — rather than
|
|
312
|
+
hiding a generic handler behind a wildcard.
|
|
313
|
+
|
|
314
|
+
- **Access is deny-by-default.** An action with no rule returns 403. For a
|
|
315
|
+
generated API that's the only safe default: you opt routes *open*, never shut,
|
|
316
|
+
so forgetting a rule fails closed rather than publishing your table.
|
|
317
|
+
- **Filtering and sorting are allow-listed.** A column not on the list is silently
|
|
318
|
+
ignored, never passed to SQL — which is what stops `?sort=password` or
|
|
319
|
+
`?secret_column=x` from doing anything. `perPage` is clamped to a ceiling, so
|
|
320
|
+
there's no "give me everything".
|
|
321
|
+
- **`scope` is row-level security, not decoration.** A row outside the scope 404s
|
|
322
|
+
for *read, update and delete* — not merely absent from the list — so it can't be
|
|
323
|
+
fetched, changed, or removed by guessing its id. There are tests for each of
|
|
324
|
+
those three, asserting against the database that the row really wasn't touched.
|
|
325
|
+
- Writes run through the model's mass-assignment guard *and* your Zod schema
|
|
326
|
+
(`body`, or `createBody`/`updateBody`), with `beforeWrite` to set fields the
|
|
327
|
+
client never sends (an owner id, timestamps).
|
|
328
|
+
- `transform` shapes the output (a function or a Keel `Transformer`); `only` /
|
|
329
|
+
`except` trim the action set; `ApiServiceProvider` publishes a `config/api.ts`
|
|
330
|
+
for the pagination defaults.
|
|
331
|
+
|
|
332
|
+
The api package **does not import openapi** — it writes its operation docs under a
|
|
333
|
+
known route-config key, so the two install independently. A test asserts that key
|
|
334
|
+
still matches the one openapi reads, because a silent drift there would stop
|
|
335
|
+
documenting every generated route and no comment would catch it.
|
|
336
|
+
|
|
337
|
+
## [0.74.4] — 2026-07-11
|
|
338
|
+
|
|
339
|
+
### Fixed
|
|
340
|
+
|
|
341
|
+
- **`flag.string({ parse })` and friends now infer their parameter.** The `const`
|
|
342
|
+
generic on the option builders swallowed the contextual type in the emitted
|
|
343
|
+
declarations, so a consumer writing `parse: (raw) => raw.toUpperCase()` would have
|
|
344
|
+
had to annotate `raw` by hand even though it compiled fine inside this repo. Same
|
|
345
|
+
bug, same fix, as `envVar`'s `validate`.
|
|
346
|
+
|
|
347
|
+
### Changed
|
|
348
|
+
|
|
349
|
+
- **The test suite is type-checked.** `tests/` was not in any tsconfig's `include`,
|
|
350
|
+
and tests run through `tsx`, which strips types without checking them — so the
|
|
351
|
+
suite had **44 type errors nobody could see**, and a test asserting a type at
|
|
352
|
+
compile time was asserting nothing at all. All 44 are fixed, and
|
|
353
|
+
`npm run typecheck` now covers `src` *and* `tests`.
|
|
354
|
+
|
|
355
|
+
- **`npm run verify:release` builds from what is committed**, not from the working
|
|
356
|
+
tree. `npm test` and `npm run typecheck` both run against your working directory,
|
|
357
|
+
so neither can see a file you forgot to commit or committed half-written — while a
|
|
358
|
+
git install runs the build through `prepare`, so a broken tree there means the
|
|
359
|
+
package cannot be installed at all. That is exactly how v0.74.0–v0.74.2 shipped
|
|
360
|
+
unusable. This exports `HEAD` and does the install a consumer would.
|
|
361
|
+
|
|
362
|
+
## [0.74.3] — 2026-07-11
|
|
363
|
+
|
|
364
|
+
### Fixed
|
|
365
|
+
|
|
366
|
+
- **v0.74.0–v0.74.2 did not build from a clean checkout**, which made them unusable
|
|
367
|
+
as a dependency (a git install runs `npm run build` through `prepare`). An
|
|
368
|
+
in-progress `src/api` feature was committed by accident, half-written, and it
|
|
369
|
+
didn't compile; a leftover `cp src/api/…` in the build script then failed once the
|
|
370
|
+
feature was untracked.
|
|
371
|
+
|
|
372
|
+
It is now untracked again — the package exports and the build config no longer
|
|
373
|
+
include it — so the released tree is back to what it was in 0.73.0 plus the
|
|
374
|
+
environment validation that 0.74.0 was actually about. A clean-clone build is
|
|
375
|
+
verified before tagging now, rather than after.
|
|
376
|
+
|
|
377
|
+
## [0.74.0] — 2026-07-11
|
|
378
|
+
|
|
379
|
+
### Added
|
|
380
|
+
|
|
381
|
+
- **Environment validation — fail at boot, not at 3am.** `env("DATABASE_URL")`
|
|
382
|
+
hands back whatever is (or isn't) in `process.env`, so a missing variable boots a
|
|
383
|
+
perfectly healthy-looking app that dies on the first request that needs it, in
|
|
384
|
+
production, at night. `defineEnv()` checks the whole environment up front and
|
|
385
|
+
**refuses to start** otherwise.
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
export const env = defineEnv({
|
|
389
|
+
APP_KEY: envVar.string({ required: true, description: "32+ random characters" }),
|
|
390
|
+
PORT: envVar.number({ default: 3000 }),
|
|
391
|
+
NODE_ENV: envVar.enum(["development", "test", "production"], { default: "development" }),
|
|
392
|
+
DATABASE_URL: envVar.url({ required: true }),
|
|
393
|
+
SENTRY_DSN: envVar.string(),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
env.PORT; // number — not "3000"
|
|
397
|
+
env.NODE_ENV; // "development" | "test" | "production" — not string
|
|
398
|
+
env.SENTRY_DSN; // string | undefined
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
- The value types are **inferred from the rules**: a `number` rule gives a
|
|
402
|
+
`number`, an `enum` gives the literal union rather than `string`, and anything
|
|
403
|
+
optional without a default is `| undefined` — so you can't forget to handle it.
|
|
404
|
+
- **Every problem is reported at once**, not the first one. Fixing a deploy one
|
|
405
|
+
missing variable per restart is its own small hell.
|
|
406
|
+
- Rules: `envVar.string/number/boolean/enum/url`, each with `required`, `default`,
|
|
407
|
+
`description` (shown in the failure, so they know what to set), and `validate`.
|
|
408
|
+
`url` catches a truncated connection string; `boolean` accepts the spellings
|
|
409
|
+
people actually use (`1`, `yes`, `on`).
|
|
410
|
+
- **An empty string counts as absent** — `PORT=` in a `.env` is a typo, not a
|
|
411
|
+
deliberate empty port.
|
|
412
|
+
- The returned object is frozen, so nothing reassigns your config at runtime.
|
|
413
|
+
|
|
414
|
+
## [0.73.0] — 2026-07-11
|
|
415
|
+
|
|
416
|
+
### Added
|
|
417
|
+
|
|
418
|
+
- **Database transactions.** `transaction(fn)` commits when `fn` returns and
|
|
419
|
+
**rolls back if it throws** — so two related writes either both land or neither
|
|
420
|
+
does, and a failure between them can't leave the card charged and the order
|
|
421
|
+
missing. The error is rethrown after the rollback; nothing is swallowed.
|
|
422
|
+
- **Queries inside are ambient.** `db()`, models, and relations all pick up the
|
|
423
|
+
open transaction without being handed it, because it lives in
|
|
424
|
+
`AsyncLocalStorage` rather than a module global — so two requests running
|
|
425
|
+
transactions at once can't steal each other's connection. `transaction()` also
|
|
426
|
+
passes an explicit handle (`tx.table()`, `tx.write()`, `tx.rollback()`) for
|
|
427
|
+
when you'd rather be obvious, and `inTransaction()` reports whether one is open.
|
|
428
|
+
- **Nesting uses savepoints.** A `transaction()` inside another doesn't open a
|
|
429
|
+
second transaction — databases don't have those — it takes a savepoint, so an
|
|
430
|
+
inner failure rolls back only the inner work and the outer transaction carries
|
|
431
|
+
on. Without that, a nested helper's failure would silently abandon its caller's
|
|
432
|
+
writes too.
|
|
433
|
+
- **The pooling trap, closed.** A transaction needs every statement on *one*
|
|
434
|
+
connection, but a pool hands each statement to whichever is free — so `BEGIN`
|
|
435
|
+
issued through a pool wraps nothing, the `COMMIT` commits nothing, and a
|
|
436
|
+
failure half-writes. It looks like it works. `Connection` therefore gains an
|
|
437
|
+
optional `begin()`: the Postgres adapter now checks a connection out of the
|
|
438
|
+
`Pool` (detected via `connect()`), runs the whole transaction on it, and
|
|
439
|
+
releases it afterwards **even if the `COMMIT` throws**. Single-connection
|
|
440
|
+
drivers (a bare `pg.Client`, SQLite, libSQL) need nothing and fall back to
|
|
441
|
+
`BEGIN`/`COMMIT`/`ROLLBACK`.
|
|
442
|
+
- **D1 refuses honestly.** Cloudflare D1 can't hold a transaction open across
|
|
443
|
+
awaits, so `transaction()` on it throws a clear error pointing at
|
|
444
|
+
`database.batch([...])`, rather than letting a `BEGIN` fail cryptically inside
|
|
445
|
+
the driver. A transaction that quietly isn't one is far worse than one that
|
|
446
|
+
refuses to start.
|
|
447
|
+
|
|
448
|
+
## [0.72.0] — 2026-07-11
|
|
449
|
+
|
|
450
|
+
### Added
|
|
451
|
+
|
|
452
|
+
- **A real console.** Commands with **typed arguments and flags**, prompts, a
|
|
453
|
+
terminal UI, and a REPL. `keel make:command greet` scaffolds one; anything in
|
|
454
|
+
`app/Commands` is discovered automatically. See the
|
|
455
|
+
[console guide](https://keeljs.com/docs/console).
|
|
456
|
+
- **The types are inferred from the spec, not cast.** `arg.string()` gives you a
|
|
457
|
+
`string`; `arg.string({ required: false })` gives you `string | undefined`; add
|
|
458
|
+
a default and it's a `string` again. The parsing is generated from the same
|
|
459
|
+
declaration, so the two can't drift apart.
|
|
460
|
+
- `arg.string/number/spread` and `flag.boolean/string/number/array`, each with
|
|
461
|
+
`description`, `default`, `required`, `parse`, and (for flags) a single-letter
|
|
462
|
+
`alias`. The parser handles `--flag value`, `--flag=value`, `--no-flag`,
|
|
463
|
+
`-f value`, bundled shorthands (`-lt 5`), and `--` passthrough.
|
|
464
|
+
- **An unknown flag is an error, not a shrug** — a typo'd `--forse` should tell
|
|
465
|
+
you rather than silently doing nothing. `allowUnknownFlags` opts out.
|
|
466
|
+
- Usage errors print what's wrong **and the command's help**; a thrown error exits
|
|
467
|
+
1 with its message, not a stack trace. A console is a bad place to show someone
|
|
468
|
+
a stack because they mistyped a flag.
|
|
469
|
+
- **Terminal UI**: `info` / `success` / `warning` / `error`, `action()` for
|
|
470
|
+
aligned CREATE/SKIP lines, tables, stickers, numbered instructions, colors, and
|
|
471
|
+
a task runner that **stops at the first failure** (the tasks after it almost
|
|
472
|
+
certainly depended on it, and a cascade of red tells you nothing new). No
|
|
473
|
+
dependency — ANSI codes are a dozen escape sequences, not a package.
|
|
474
|
+
- **Prompts**: `ask`, `secure`, `confirm`, `toggle`, `choice`, `multiple`,
|
|
475
|
+
`autocomplete`, with `default` / `hint` / `validate` / `result`. A failed
|
|
476
|
+
`validate` re-asks rather than dying.
|
|
477
|
+
- **Prompts are testable**, which is the whole point: `createPrompt({ trap: true })`
|
|
478
|
+
lets a test script the answers, and `createUi({ raw: true })` buffers the output
|
|
479
|
+
colorlessly so you can assert on exactly what a command said. An **untrapped
|
|
480
|
+
prompt throws instead of hanging** — otherwise a test would block forever on
|
|
481
|
+
stdin it will never receive, and the suite would just stop, with no failure to
|
|
482
|
+
read. `assertAllTrapsUsed()` catches a question you scripted but never asked.
|
|
483
|
+
- **`keel repl`** — an interactive shell with the application booted: the
|
|
484
|
+
container is up, the providers have run, and `db`, `make`, `cache`, `router`
|
|
485
|
+
and friends are in scope. `.ls` lists them. Poking at a model in a REPL is the
|
|
486
|
+
fastest debugging loop there is, and it shouldn't cost you a throwaway script.
|
|
487
|
+
|
|
488
|
+
The built-in commands (`serve`, `routes`, `make:*`, `migrate:*`) and
|
|
489
|
+
package-contributed commands still run through the original console wrapper; your
|
|
490
|
+
commands run on the new system and take precedence over a built-in of the same
|
|
491
|
+
name. Migrating the built-ins across is mechanical and changes none of the API
|
|
492
|
+
above.
|
|
493
|
+
|
|
494
|
+
## [0.71.0] — 2026-07-11
|
|
495
|
+
|
|
496
|
+
### Added
|
|
497
|
+
|
|
498
|
+
- **Pages — page-based routing, where a file *is* a route.** `resources/pages/users/[id].tsx`
|
|
499
|
+
serves `/users/:id`; no route file to keep in sync, no controller, no wiring. New
|
|
500
|
+
[pages guide](https://keeljs.com/docs/pages), and `keel make:page users/[id]`.
|
|
501
|
+
- Conventions: `index.tsx` names its directory, `[id]` is a parameter,
|
|
502
|
+
`[...slug]` is a catch-all, and a leading `_` keeps a file private — so a
|
|
503
|
+
layout or a partial can live beside your pages without becoming a URL.
|
|
504
|
+
- **`loader`** runs before the page renders and its return value arrives as
|
|
505
|
+
`data`; **`middleware`** guards a page (and runs *before* the loader, so a
|
|
506
|
+
refused page never loads its data); **`name`** and **`path`** override the
|
|
507
|
+
derived route name and URL.
|
|
508
|
+
- **Specificity is decided for you.** This is the part file-based routing
|
|
509
|
+
usually gets wrong: register `/users/:id` before `/users/new` and the literal
|
|
510
|
+
page is unreachable forever, because `:id` matches `"new"`. Pages are sorted
|
|
511
|
+
before they're registered — literals beat parameters, parameters beat
|
|
512
|
+
catch-alls — so the file layout stops being a trap.
|
|
513
|
+
- **It drives the router rather than replacing it.** Every page becomes an
|
|
514
|
+
ordinary named route, so `url()` finds it, route middleware applies, and
|
|
515
|
+
`keel routes` lists it. Mix pages and hand-written routes freely, and reach for
|
|
516
|
+
a controller the moment a page outgrows a file.
|
|
517
|
+
- Edge-safe: `pages()` scans the filesystem on Node, while `definePages()` takes
|
|
518
|
+
a build-time manifest — `import.meta.glob("./pages/**/*.tsx", { eager: true })`
|
|
519
|
+
— so the same pages run on Workers.
|
|
520
|
+
|
|
521
|
+
- **Packages — a redistributable slice of an app.** Routes, a UI, config,
|
|
522
|
+
migrations, and console commands that install with a single `app.register(...)`.
|
|
523
|
+
`ServiceProvider` was already the unit of composition; `PackageProvider` adds the
|
|
524
|
+
conventions a *shippable* package needs, so it can carry its own schema and
|
|
525
|
+
assets instead of asking the host app to wire them by hand. `MigrationRegistry`,
|
|
526
|
+
`CommandRegistry`, `PublishRegistry`. New [packages guide](https://keeljs.com/docs/packages).
|
|
527
|
+
|
|
528
|
+
- **Watch — a debug dashboard.** Records the requests, queries, exceptions, logs,
|
|
529
|
+
jobs, mail, notifications, cache lookups, events, and scheduled tasks flowing
|
|
530
|
+
through the app, and shows them at `/watch`, with each request linked to the
|
|
531
|
+
queries and logs it produced. Built on a new instrumentation seam (`instrument()`,
|
|
532
|
+
`runRequest()`, `currentRequestId()`) and on `tapLogs()`, which observes every log
|
|
533
|
+
record without changing where logs normally go. Ships as a Keel package, and is
|
|
534
|
+
that system's reference implementation. New [watch guide](https://keeljs.com/docs/watch).
|
|
535
|
+
|
|
536
|
+
## [0.70.0] — 2026-07-11
|
|
537
|
+
|
|
538
|
+
### Added
|
|
539
|
+
|
|
540
|
+
- **Telemetry — distributed tracing with no SDK.** Spans, W3C trace context, and an
|
|
541
|
+
OTLP exporter, in a module you can read. The OpenTelemetry Node SDK is a large
|
|
542
|
+
tree of packages that assumes a Node process; what a trace *is*, though, is
|
|
543
|
+
small — an id, a parent, a start and an end, some attributes, and a documented
|
|
544
|
+
JSON shape to POST them in. This speaks **OTLP/HTTP over `fetch`**, so it runs on
|
|
545
|
+
Workers as happily as on Node, and any collector takes it (Jaeger, Tempo,
|
|
546
|
+
Honeycomb, Grafana, Datadog). New [telemetry guide](https://keeljs.com/docs/telemetry).
|
|
547
|
+
- `trace(name, fn)` opens a span, ends it when `fn` settles, and records a throw
|
|
548
|
+
before rethrowing. Spans **nest automatically** across `await` boundaries — and
|
|
549
|
+
across *concurrent* traces, because the current span lives in
|
|
550
|
+
`AsyncLocalStorage`, not a global, so two in-flight requests can't get tangled.
|
|
551
|
+
- `tracing()` middleware: a server span per request, joined to the caller's trace
|
|
552
|
+
via their `traceparent`, with the trace id written back on the response so a
|
|
553
|
+
user reporting a slow page can be looked up. A 5xx fails the span; a 404
|
|
554
|
+
doesn't — that's a valid answer, not a fault.
|
|
555
|
+
- `injectTraceContext()` for outgoing calls, plus `parseTraceparent()` /
|
|
556
|
+
`traceparent()`. A malformed header starts a fresh trace rather than failing
|
|
557
|
+
the request.
|
|
558
|
+
- `traceIds()` to hang `trace_id` / `span_id` on a log line — the jump from a log
|
|
559
|
+
to the trace it came from.
|
|
560
|
+
- `sampleRatio`, decided **once at the root and inherited by every child**,
|
|
561
|
+
because half a trace is worse than none.
|
|
562
|
+
- `otlpExporter()`, `consoleExporter()`, and `MemoryExporter` for tests. Spans
|
|
563
|
+
batch; `flushTelemetry()` drains them before an isolate goes away.
|
|
564
|
+
|
|
565
|
+
- **A real testing toolkit.** The test client injects requests without a server;
|
|
566
|
+
this fills in everything around it. See the
|
|
567
|
+
[testing guide](https://keeljs.com/docs/testing).
|
|
568
|
+
- **Request building:** `withToken()`, `withBasicAuth()`, `withHeader(s)`,
|
|
569
|
+
`withCookie(s)`, `acceptJson()` — each returning a **copy**, so a configured
|
|
570
|
+
client can't leak into another test — plus `form()` and `multipart()`.
|
|
571
|
+
- **Response assertions:** `assertJsonContains()` (a subset match — pins the
|
|
572
|
+
fields a test is about, so adding an unrelated field doesn't break twenty
|
|
573
|
+
tests), `assertSee()` / `assertDontSee()`, `assertValidationErrors(...fields)`,
|
|
574
|
+
`assertCookie()` / `assertCookieMissing()`, `assertHeaderMissing()`, status
|
|
575
|
+
shorthands (`assertCreated`, `assertNotFound`, `assertUnprocessable`, …), and
|
|
576
|
+
`dump()`.
|
|
577
|
+
- **Database assertions:** `assertDatabaseHas()` / `assertDatabaseMissing()` /
|
|
578
|
+
`assertDatabaseCount()` / `assertDatabaseEmpty()`, and `truncate()` — which
|
|
579
|
+
deletes rows rather than rolling back a transaction, so it works on every
|
|
580
|
+
driver rather than only the ones with savepoints.
|
|
581
|
+
- **Time control:** `freezeTime()` / `timeTravel()` / `restoreTime()`, so
|
|
582
|
+
"expires in an hour" doesn't take an hour to test. Mocks `Date` and `Date.now()`
|
|
583
|
+
— not timers, and not `new Date("2020-01-01")`; only "what time is it *now*".
|
|
584
|
+
- **Spies:** `spy()` and `spyOn()`, which **call through by default** — observing
|
|
585
|
+
rather than stubbing — until you tell them otherwise. `restoreSpies()` undoes
|
|
586
|
+
them.
|
|
587
|
+
- **State reset:** `resetState()` restores every fake, unfreezes the clock, drops
|
|
588
|
+
event listeners, empties the cache, and hands back a fresh lock store.
|
|
589
|
+
- **Console tests:** `runCommand(fn)` captures stdout, stderr, and the exit code,
|
|
590
|
+
with `assertSucceeded()` / `assertFailed()` / `assertOutputContains()` and
|
|
591
|
+
friends. You pass the command *in*, because the console entry point belongs to
|
|
592
|
+
your app, not the core.
|
|
593
|
+
- **Browser tests** are documented rather than wrapped: Playwright already does
|
|
594
|
+
this well, and a thinner API in front of it would only get in the way.
|
|
595
|
+
|
|
596
|
+
## [0.69.1] — 2026-07-11
|
|
597
|
+
|
|
598
|
+
### Fixed
|
|
599
|
+
|
|
600
|
+
- **`serveStorage({ signed: true })` now fails loudly on a `basePath` mismatch.**
|
|
601
|
+
`signedUrl()` signs the path the *disk* reports, so a disk handing out
|
|
602
|
+
`/storage/…` while the middleware is mounted at `/private` could never produce a
|
|
603
|
+
matching signature — and every request 403'd, which reads as "your link expired"
|
|
604
|
+
and sends you hunting in the wrong place. It now throws, naming both paths and
|
|
605
|
+
how to line them up.
|
|
606
|
+
|
|
607
|
+
## [0.69.0] — 2026-07-11
|
|
608
|
+
|
|
609
|
+
### Added
|
|
610
|
+
|
|
611
|
+
- **AI-native tooling — write Keel apps with an agent.** A machine-readable
|
|
612
|
+
surface generated from the same source as the human docs, so it never drifts:
|
|
613
|
+
- **An MCP server** (`keel mcp` / the shipped `keel-mcp` bin) exposing Keel's
|
|
614
|
+
docs, full public API (400+ exports), generators, and conventions to any
|
|
615
|
+
[Model Context Protocol](https://modelcontextprotocol.io) client. Tools:
|
|
616
|
+
`keel_overview`, `keel_search_docs`, `keel_read_doc`, `keel_search_api`,
|
|
617
|
+
`keel_list_generators`, `keel_scaffold`. Resources: `keel://overview`,
|
|
618
|
+
`keel://llms-full`, and `keel://docs/<slug>` per guide.
|
|
619
|
+
- **`AGENTS.md` + `CLAUDE.md`** — an agent playbook (import rule, folder map,
|
|
620
|
+
container/provider model, how-to-add-X table, guardrails), shipped in the
|
|
621
|
+
package.
|
|
622
|
+
- **`llms.txt` + `llms-full.txt`** — a [spec-compliant](https://llmstxt.org)
|
|
623
|
+
doc index and a one-file concatenation of every guide, shipped in the package.
|
|
624
|
+
- **`docs/ai.md`** — the "Building Keel apps with AI" guide.
|
|
625
|
+
- **`npm run build:ai`** regenerates `llms.txt`, `llms-full.txt`, and
|
|
626
|
+
`docs/ai-manifest.json` (the index the MCP server reads); wired into `build`.
|
|
627
|
+
- **Distributed locks** (`lock()`, `MemoryLockStore`, `LockStore`) — "only one
|
|
628
|
+
of you may do this at a time" across processes and nodes, with a pluggable
|
|
629
|
+
store seam (the core imports no driver). See the locks guide.
|
|
630
|
+
|
|
631
|
+
Every acquisition mints an owner token, and release/extend only succeed for the
|
|
632
|
+
owner. That isn't bookkeeping — without it, a lock whose TTL expires mid-work
|
|
633
|
+
gets picked up by process B, and A's late `release()` would delete **B's** lock
|
|
634
|
+
and let a third process in. `run()` takes the lock, runs, and always gives it
|
|
635
|
+
back; `extend()` throws rather than silently no-op'ing once the lock is lost.
|
|
636
|
+
|
|
637
|
+
- **Internationalization** — ICU message formatting plus the `Intl` formatters
|
|
638
|
+
that go with it, with **no dependency**: Node and Workers both ship full ICU, so plurals,
|
|
639
|
+
currencies, dates, and relative times are the platform's job, and Keel only adds
|
|
640
|
+
the message parser on top.
|
|
641
|
+
- `t(key, data)` / `i18n(locale)`, with an ICU subset covering interpolation,
|
|
642
|
+
`plural` (including exact `=0` branches and `#`), `selectordinal`, `select`,
|
|
643
|
+
`number` (incl. `::currency/USD`), `date`, and `time` — nested arbitrarily
|
|
644
|
+
deep. Plural categories come from the **locale**, not from English.
|
|
645
|
+
- `Intl`-backed `formatNumber` / `formatCurrency` / `formatDate` / `formatTime` /
|
|
646
|
+
`formatRelativeTime` / `formatList` / `formatPlural` / `formatDisplayName` —
|
|
647
|
+
worth using even in a single-locale app.
|
|
648
|
+
- `detectLocale()` middleware (custom resolver → query → cookie →
|
|
649
|
+
`Accept-Language` → default; only **supported** locales are honored, so
|
|
650
|
+
`?lang=xx` can't push the app into a locale you have no translations for) and
|
|
651
|
+
`negotiateLocale()` on its own.
|
|
652
|
+
- Nested or flat translation keys, and a fallback chain that walks `es-MX` →
|
|
653
|
+
configured fallback → `es` → default, so a regional locale can be a handful of
|
|
654
|
+
overrides.
|
|
655
|
+
- A missing key renders as the key itself (the page still works and the gap is
|
|
656
|
+
visible) and fires `i18n.missing`.
|
|
657
|
+
|
|
658
|
+
- **Mail: queueing, attachments, class-based mails, and a fake.**
|
|
659
|
+
- **`sendLater()`** — put the message on the queue instead of holding the request
|
|
660
|
+
open for an SMTP round trip. Validated at the call site, not on the worker, so
|
|
661
|
+
a malformed message throws where the stack trace means something.
|
|
662
|
+
- **Attachments** — `attach()` (content type inferred from the extension) and
|
|
663
|
+
`embed()` for inline `cid:` images.
|
|
664
|
+
- **`BaseMail`** — a reusable, testable email class; `send()` / `sendLater()`.
|
|
665
|
+
- **Named mailers** — `setMailer(t, o, "marketing")` / `mail("marketing")`.
|
|
666
|
+
- **`fakeMail()` / `restoreMail()`** with `assertSent` / `assertNotSent` /
|
|
667
|
+
`assertSentCount` / `assertQueued` / `assertNotQueued` / `assertQueuedCount` /
|
|
668
|
+
`assertNothingSent`. Sent and queued are tracked separately, and the fake still
|
|
669
|
+
validates, so it can't paper over a message the real mailer would reject.
|
|
670
|
+
- `mail.sending` / `mail.sent` / `mail.queued` events, and a default `replyTo`.
|
|
671
|
+
|
|
672
|
+
- **Queues: retries, backoff, priority, and a dead-letter list.**
|
|
673
|
+
- **Retries with backoff** — `static maxRetries` and `static backoff` per job
|
|
674
|
+
class (`exponentialBackoff` / `linearBackoff` / `fixedBackoff` / `noBackoff`),
|
|
675
|
+
overridable per dispatch. `maxRetries` defaults to 0 — the safe default for
|
|
676
|
+
work that isn't idempotent.
|
|
677
|
+
- **`failed(error)` hook** and a **dead-letter list** (`driver.failed`): an
|
|
678
|
+
exhausted job is logged, handed to its hook, and kept, rather than vanishing.
|
|
679
|
+
- **Priority** (lower runs first) and per-class `queue` / `priority` defaults.
|
|
680
|
+
- **`JobContext`** (`jobId`, `attempt`, `queue`) readable from `handle()`.
|
|
681
|
+
- **`fakeQueue()` / `restoreQueue()`** with `assertPushed` / `assertNotPushed` /
|
|
682
|
+
`assertPushedCount` / `assertNothingPushed` / `pushedJobs`.
|
|
683
|
+
|
|
684
|
+
- **Logger: `trace` and `fatal`, sinks, and better redaction.**
|
|
685
|
+
- Levels are now `trace` < `debug` < `info` < `warn` < `error` < `fatal`, plus
|
|
686
|
+
`log(level, …)`, `isLevelEnabled()` / `ifLevelEnabled()` (so an expensive
|
|
687
|
+
context object isn't built for a line nobody will emit), and `enabled: false`.
|
|
688
|
+
- **Sinks** — output goes through a `Sink` function receiving the structured
|
|
689
|
+
`LogRecord`, so logs can go to a file or an HTTP collector instead of the
|
|
690
|
+
console. `MemorySink` collects them for tests.
|
|
691
|
+
- **Redaction** gains `*` wildcard path segments (`"*.password"`), a custom
|
|
692
|
+
`censor`, and `remove` to drop the key outright. It still never mutates the
|
|
693
|
+
caller's object, and it runs *before* the sink, so a custom sink can never see
|
|
694
|
+
the unredacted values.
|
|
695
|
+
- **Named loggers** — `setLogger(logger, "audit")` / `namedLogger("audit")`.
|
|
696
|
+
|
|
697
|
+
- **`hasApplication()`** — whether an application has been bootstrapped. The queue
|
|
698
|
+
and the mailer use it so they still work in a worker or a unit test that never
|
|
699
|
+
created one.
|
|
700
|
+
|
|
701
|
+
### Changed
|
|
702
|
+
|
|
703
|
+
- **A failed queue job no longer takes down the worker.** `work()` used to
|
|
704
|
+
propagate the error and stop draining. It now retries the job, and once the
|
|
705
|
+
retries are exhausted it logs the failure loudly, runs `failed()`, records it in
|
|
706
|
+
the driver's dead-letter list, and **carries on with the rest of the queue** —
|
|
707
|
+
one bad job can't stop the others. `SyncDriver` is the exception: it ran the job
|
|
708
|
+
*inline*, so the caller still gets the error thrown at them.
|
|
709
|
+
|
|
710
|
+
- **`dispatch()` now materializes the queue defaults**, so a queued job's `options`
|
|
711
|
+
always carry `queue` and `priority`.
|
|
712
|
+
|
|
713
|
+
## [0.68.0] — 2026-07-11
|
|
714
|
+
|
|
715
|
+
### Added
|
|
716
|
+
|
|
717
|
+
- **Storage: signed URLs, direct uploads, and metadata.** Kept keel's "core imports
|
|
718
|
+
no SDK" rule — the `Disk` seam grew *optional* capabilities, so existing disks
|
|
719
|
+
keep working untouched:
|
|
720
|
+
- **Content types.** `put()` now infers the MIME type from the path's extension
|
|
721
|
+
(`.png` → `image/png`), so files stop landing in your bucket as
|
|
722
|
+
`application/octet-stream` — the difference between a browser *rendering* a
|
|
723
|
+
file and *downloading* it. New `WriteOptions` (`contentType`, `cacheControl`,
|
|
724
|
+
`visibility`, custom `metadata`) override it.
|
|
725
|
+
- **`signedUrl(path, { expiresIn })`** — a temporary URL for a private file. A
|
|
726
|
+
disk with backend presigning (S3/R2/GCS) returns the bucket's own; any other
|
|
727
|
+
disk gets one signed with `config('app.key')`. The signature covers the path
|
|
728
|
+
and query but **not the host**, so a signed URL survives a CDN hostname.
|
|
729
|
+
- **`signedUploadUrl(path, { contentType })`** — the browser `PUT`s straight to
|
|
730
|
+
the bucket, so a 50 MB upload never streams through a Worker. Requires a disk
|
|
731
|
+
that can presign; there's no generic fallback, and calling it on one that
|
|
732
|
+
can't throws rather than handing back a URL that won't work.
|
|
733
|
+
- **`serveStorage()`** — middleware that serves a disk's files over HTTP with
|
|
734
|
+
`ETag`/304 and stored `Cache-Control`, and (in `signed: true` mode) rejects
|
|
735
|
+
unsigned or expired requests with a 403. This is what makes app-signed URLs
|
|
736
|
+
real for disks without backend presigning.
|
|
737
|
+
- **`metadata()` / `size()` / `copy()` / `move()`** — using the backend's
|
|
738
|
+
server-side operation when the disk offers one, falling back to
|
|
739
|
+
read-then-write otherwise.
|
|
740
|
+
- **`fakeDisk()` / `restoreDisk()`** with `assertExists` / `assertMissing` /
|
|
741
|
+
`assertContents` / `assertCount`, so tests never touch a real bucket. Matches
|
|
742
|
+
the `hash.fake()` precedent from 0.66.0.
|
|
743
|
+
- Also `signStorageUrl` / `verifyStorageUrl` / `contentTypeFor` for signing any
|
|
744
|
+
URL yourself, and an S3/R2 presigning disk recipe in the
|
|
745
|
+
[storage guide](https://keeljs.com/docs/storage).
|
|
746
|
+
|
|
747
|
+
- **Events: a typed registry, error isolation, and fakes.**
|
|
748
|
+
- **The `EventsList` registry.** Declare an event's payload once via module
|
|
749
|
+
augmentation and *both* sides are checked — the value you `emit` and the one
|
|
750
|
+
your listener receives can no longer drift apart. Opt-in and incremental: an
|
|
751
|
+
undeclared event behaves exactly as before.
|
|
752
|
+
- **`onError(handler)`** — route listener failures to one handler (with the
|
|
753
|
+
event name and payload) instead of letting them reject `emit`.
|
|
754
|
+
- **`onAny(listener)`** — observe every event, for logging and metrics.
|
|
755
|
+
- **`fake()` / `restore()`** returning an `EventBuffer` with `assertEmitted`
|
|
756
|
+
(optionally payload-matching), `assertNotEmitted`, `assertEmittedCount`,
|
|
757
|
+
`assertNoneEmitted`, `all()`, and `payloadsFor()` — assert an event fired
|
|
758
|
+
without triggering its side effects.
|
|
759
|
+
- **`clearAll()`** — drop listeners, any-listeners, and the error handler.
|
|
760
|
+
|
|
761
|
+
- **Health checks.** New [health guide](https://keeljs.com/docs/health) and
|
|
762
|
+
`healthCheck()` middleware serving the two endpoints an orchestrator actually
|
|
763
|
+
asks about: `/health/live` (answers instantly, checks **nothing** — a liveness
|
|
764
|
+
probe that touched the database would get a healthy app restarted during a
|
|
765
|
+
database blip) and `/health/ready` (runs every registered check; **200** while
|
|
766
|
+
healthy, **503** when one fails, which evicts the instance without killing it).
|
|
767
|
+
`health().register([...])`, `Result.ok/warning/failed` (a warning is still
|
|
768
|
+
healthy), `withMeta()`, `cacheFor(seconds)` so a frequent probe doesn't hammer
|
|
769
|
+
what it's probing, `check(name, fn)` and `BaseCheck` for your own, and built-in
|
|
770
|
+
`DatabaseCheck` / `RedisCheck` / `CacheCheck`. A check that throws becomes a
|
|
771
|
+
failed result rather than taking down the report.
|
|
772
|
+
|
|
773
|
+
Deliberately **absent**: disk-space, heap, and RSS checks. They measure a Node
|
|
774
|
+
process, and on Workers there isn't one.
|
|
775
|
+
|
|
776
|
+
### Changed
|
|
777
|
+
|
|
778
|
+
- **A throwing event listener no longer skips the listeners after it.** `emit()`
|
|
779
|
+
now runs *every* listener and reports failures afterwards — rejecting with the
|
|
780
|
+
error, or with an `AggregateError` if several failed, or handing them to
|
|
781
|
+
`onError()` if one is registered. Previously the first failure aborted the loop,
|
|
782
|
+
so an analytics listener blowing up could silently cancel the welcome email.
|
|
783
|
+
Failures are still never swallowed.
|
|
784
|
+
|
|
785
|
+
[0.68.0]: https://github.com/shaferllc/keel/releases/tag/v0.68.0
|
|
786
|
+
|
|
787
|
+
## [0.67.0] — 2026-07-11
|
|
788
|
+
|
|
789
|
+
### Added
|
|
790
|
+
|
|
791
|
+
- **Cache resilience & invalidation.** Stayed inside keel's single-store,
|
|
792
|
+
edge-native model:
|
|
793
|
+
- **Stampede protection.** `remember()` / `rememberForever()` now collapse
|
|
794
|
+
concurrent misses for the same key into a **single** factory run, sharing the
|
|
795
|
+
result — a hot key expiring no longer dog-piles the upstream. Per-isolate (no
|
|
796
|
+
cross-node lock), which is where the dog-pile actually melts a server.
|
|
797
|
+
- **Grace / stale-on-error.** `remember(key, ttl, factory, { grace })` retains
|
|
798
|
+
an expired value `grace` seconds longer and serves it if the refreshing
|
|
799
|
+
factory **throws** — a flaky upstream degrades to slightly-stale data instead
|
|
800
|
+
of an error. A plain `get()` still reports the expired key as a miss, so stale
|
|
801
|
+
values never leak through the read path.
|
|
802
|
+
- **Tags & `deleteByTag`.** Tag entries via a `{ tags }` option on
|
|
803
|
+
`put`/`add`/`remember`/`rememberForever`, then invalidate a whole group with
|
|
804
|
+
`deleteByTag(["posts"])`. Uses version-stamp invalidation (a per-tag counter
|
|
805
|
+
entries record and `deleteByTag` bumps), so it's O(number of tags) with no key
|
|
806
|
+
index and works on any `CacheStore`. Tag-dropped entries are a hard miss (not
|
|
807
|
+
grace-eligible).
|
|
808
|
+
- **Namespaces.** `cache().namespace("users")` scopes keys under a `users:`
|
|
809
|
+
prefix (so namespaces can reuse logical keys) and its `flush()` clears **only**
|
|
810
|
+
that namespace via the same version-stamp mechanism. Namespaces nest and carry
|
|
811
|
+
the full API.
|
|
812
|
+
- **`add(key, value, ttl?)`** — write only if absent, returns whether it wrote.
|
|
813
|
+
- **`missing(key)`** — the inverse of `has`.
|
|
814
|
+
- **`forgetMany(keys)`** — delete several keys at once.
|
|
815
|
+
- New `RememberOptions` and `PutOptions` types.
|
|
816
|
+
|
|
817
|
+
Values are now stored in an internal envelope (value + logical expiry + tag
|
|
818
|
+
stamps) so the cache can distinguish fresh from grace-retained or
|
|
819
|
+
tag-invalidated; this is transparent through the `Cache` API and JSON-safe for
|
|
820
|
+
the Redis store. Existing `get`/`put`/`has`/`pull`/`remember` behavior is
|
|
821
|
+
unchanged, and the pluggable `CacheStore` contract is untouched. Intentionally
|
|
822
|
+
**not** matched from bentocache: multi-tier L1/L2 + bus (multi-node sync),
|
|
823
|
+
soft/hard timeouts, and the DynamoDB/database/file drivers — larger features
|
|
824
|
+
that cut against keel's single-store simplicity.
|
|
825
|
+
|
|
826
|
+
[0.67.0]: https://github.com/shaferllc/keel/releases/tag/v0.67.0
|
|
827
|
+
|
|
828
|
+
## [0.66.0] — 2026-07-11
|
|
829
|
+
|
|
830
|
+
### Added
|
|
831
|
+
|
|
832
|
+
- **`hash.fake()` / `hash.restore()`.** Swap real PBKDF2 for a trivial, insecure
|
|
833
|
+
scheme in tests so a suite that creates many users doesn't pay the (deliberate)
|
|
834
|
+
hashing cost — `make` returns `fake$<password>` and `verify` just compares.
|
|
835
|
+
Never for use outside tests.
|
|
836
|
+
|
|
837
|
+
[0.66.0]: https://github.com/shaferllc/keel/releases/tag/v0.66.0
|
|
838
|
+
|
|
839
|
+
## [0.65.0] — 2026-07-11
|
|
840
|
+
|
|
841
|
+
### Added
|
|
842
|
+
|
|
843
|
+
- **Security middleware suite.** Hashing and encryption already existed; this adds
|
|
844
|
+
the rest — all edge-native:
|
|
845
|
+
- **`cors()`** — Cross-Origin Resource Sharing with automatic preflight
|
|
846
|
+
handling. `origin` as boolean / `"*"` / allowlist / predicate, plus
|
|
847
|
+
`methods`, `headers`, `exposeHeaders`, `credentials` (auto-downgrades `"*"`
|
|
848
|
+
to the concrete origin), and `maxAge`. New [CORS](https://keeljs.com/docs/cors)
|
|
849
|
+
guide.
|
|
850
|
+
- **`securityHeaders()`** — the SSR "shield": Content-Security-Policy (string or
|
|
851
|
+
a camelCase directives object), HSTS, `X-Frame-Options`, `X-Content-Type-Options:
|
|
852
|
+
nosniff`, and `Referrer-Policy`, each individually toggleable.
|
|
853
|
+
- **`csrf()`** — session-backed CSRF protection; rejects unsafe requests without
|
|
854
|
+
a valid token (`419`), with `csrfField()` / `csrfToken()` helpers, an
|
|
855
|
+
`XSRF-TOKEN` cookie for SPAs, and route exemptions. New
|
|
856
|
+
[Securing SSR apps](https://keeljs.com/docs/security) guide.
|
|
857
|
+
|
|
858
|
+
- **Container & provider lifecycle.** Container services already existed as the
|
|
859
|
+
global helpers in [`helpers.ts`](https://keeljs.com/docs/container); this fills
|
|
860
|
+
in the rest:
|
|
861
|
+
- **Provider `ready()` and `shutdown()` hooks.** Providers grew two optional
|
|
862
|
+
lifecycle methods beyond `register()`/`boot()`: `ready()` runs once the whole
|
|
863
|
+
app is up (after every provider's `boot()` and the app's `onReady` hooks), and
|
|
864
|
+
`shutdown()` runs on `app.terminate()` in reverse registration order (LIFO).
|
|
865
|
+
Both are optional, so plain duck-typed providers keep working.
|
|
866
|
+
- **`Container.swap(token, factory)` / `restore(token?)`.** Temporarily replace a
|
|
867
|
+
binding with a fake for tests — the original binding and any resolved instance
|
|
868
|
+
are remembered; `restore()` with no token undoes every swap. Also as the
|
|
869
|
+
`swap` / `restore` global helpers.
|
|
870
|
+
- **`Container.alias(alias, target)`.** Point a token at another so
|
|
871
|
+
`make("router")` resolves through to `make(Router)`, honoring the target's own
|
|
872
|
+
sharing. Also as the `alias` global helper.
|
|
873
|
+
- **Graceful shutdown is now wired.** `keel serve` traps SIGINT/SIGTERM, stops
|
|
874
|
+
accepting connections, and runs `app.terminate()` (and thus every provider's
|
|
875
|
+
`shutdown()`) before exiting — the `onShutdown`/`terminate()` machinery
|
|
876
|
+
existed but nothing triggered it.
|
|
877
|
+
|
|
878
|
+
All additive and backward compatible. `@inject`-style reflective constructor
|
|
879
|
+
injection and contextual bindings were intentionally left out — Keel's DI is by
|
|
880
|
+
convention (a provider/controller constructor receives the container), which
|
|
881
|
+
keeps the core free of `reflect-metadata` and edge-native.
|
|
882
|
+
|
|
883
|
+
### Changed
|
|
884
|
+
|
|
885
|
+
- **`encryption.encrypt(value, { expiresIn, purpose })`** — encrypted values can
|
|
886
|
+
now self-expire and be bound to a purpose (e.g. `"password-reset"`), verified on
|
|
887
|
+
`decrypt(token, { purpose })`; a wrong/absent purpose or an expired token returns
|
|
888
|
+
`null`. Backward compatible — tokens made without options decrypt as before.
|
|
889
|
+
- **`rateLimiter`** now also emits the `X-RateLimit-Reset` header.
|
|
890
|
+
|
|
891
|
+
[0.65.0]: https://github.com/shaferllc/keel/releases/tag/v0.65.0
|
|
892
|
+
|
|
893
|
+
## [0.64.0] — 2026-07-11
|
|
894
|
+
|
|
895
|
+
### Added
|
|
896
|
+
|
|
897
|
+
- **OAuth 1.0a social sign-in.** Social auth grew a second flow for the older,
|
|
898
|
+
three-legged providers (Twitter/X, and any OAuth 1.0a API). Every request is
|
|
899
|
+
HMAC-SHA1-signed with Web Crypto, so it's edge-native like the OAuth2 side:
|
|
900
|
+
- `social.twitter(config)` preset, and `social.driver1(spec, config)` /
|
|
901
|
+
`oauth1Driver` for any OAuth 1.0a provider.
|
|
902
|
+
- `OAuth1Driver` — `requestToken()` → `redirect()` → `accessToken()` /
|
|
903
|
+
`user()`, plus a signed `get()` for profile calls. Returns the same
|
|
904
|
+
normalized `SocialUser` (its `token` is an `OAuth1Token`).
|
|
905
|
+
- `oauth1Signature()` — the low-level RFC 5849 HMAC-SHA1 signer, exposed for
|
|
906
|
+
signing arbitrary provider API requests (verified against the canonical
|
|
907
|
+
Twitter test vector).
|
|
908
|
+
- New types `OAuth1Config`, `OAuth1Token`, `OAuth1ProviderSpec`; `SocialUser`
|
|
909
|
+
is now generic over its token type. Documented in the
|
|
910
|
+
[Social authentication](https://keeljs.com/docs/social-auth) guide.
|
|
911
|
+
|
|
912
|
+
Additive and backward compatible — the OAuth2 presets are unchanged.
|
|
913
|
+
|
|
914
|
+
[0.64.0]: https://github.com/shaferllc/keel/releases/tag/v0.64.0
|
|
915
|
+
|
|
916
|
+
## [0.63.0] — 2026-07-11
|
|
917
|
+
|
|
918
|
+
### Added
|
|
919
|
+
|
|
920
|
+
- **A full authentication system.** Session and JWT already existed; this adds the
|
|
921
|
+
rest, all edge-native (Web Crypto + `fetch`, no native deps):
|
|
922
|
+
- **Opaque access tokens** (`tokens.ts`) — revocable, ability-scoped, DB-backed
|
|
923
|
+
bearer tokens, the stateful counterpart to `jwt`. `createToken(userId, {
|
|
924
|
+
abilities, expiresIn, name })` mints a `keel_<selector>.<verifier>` token
|
|
925
|
+
(plaintext shown once); `verifyToken`, `revokeToken`, `revokeTokens` (log out
|
|
926
|
+
everywhere), `listTokens`, `tokenAllows`/`tokenDenies`, `setTokensTable`. The
|
|
927
|
+
split selector/verifier design stores only a SHA-256 hash and needs no
|
|
928
|
+
`RETURNING`, so it's portable across every driver and a leaked DB can't mint
|
|
929
|
+
tokens. Expired tokens self-prune on use.
|
|
930
|
+
- **`tokenAuth(options?)`** guard — verifies an opaque `Bearer` token, sets the
|
|
931
|
+
authenticated user, enforces required `abilities`, and exposes the token via
|
|
932
|
+
`token()` / `tokenCan()`.
|
|
933
|
+
- **`basicAuth(verify, options?)`** guard — HTTP Basic auth with a
|
|
934
|
+
`WWW-Authenticate` challenge; the verifier returns a user id, `true`, or a
|
|
935
|
+
falsy value.
|
|
936
|
+
- **Social sign-in** (`social.ts`) — OAuth 2.0 "sign in with…", `fetch`-based
|
|
937
|
+
with GitHub/Google/Discord presets and `social.driver()` for any other
|
|
938
|
+
provider. Returns a normalized `SocialUser`; `redirect()`, `exchangeCode()`,
|
|
939
|
+
`userFromToken()`, `user()`, `oauthState()` for CSRF, `OAuthError`. Keel owns
|
|
940
|
+
the OAuth dance only — you find-or-create your user and log them in. New
|
|
941
|
+
[Social authentication](https://keeljs.com/docs/social-auth) guide.
|
|
942
|
+
- **Timing-safe credentials** — `hash.dummy`, a valid hash that never matches,
|
|
943
|
+
so verifying a missing user costs the same as a wrong password (closes the
|
|
944
|
+
email-enumeration timing leak).
|
|
945
|
+
- **`gateAfter(callback)`** — the after-hook counterpart to `gateBefore`,
|
|
946
|
+
completing authorization parity (audit or veto a decision after it's made).
|
|
947
|
+
|
|
948
|
+
All additive and backward compatible. The `Auth` session guard, `jwt` +
|
|
949
|
+
`bearerAuth`, and gates/policies are unchanged.
|
|
950
|
+
|
|
951
|
+
[0.63.0]: https://github.com/shaferllc/keel/releases/tag/v0.63.0
|
|
952
|
+
|
|
953
|
+
## [0.62.0] — 2026-07-11
|
|
954
|
+
|
|
955
|
+
### Added
|
|
956
|
+
|
|
957
|
+
- **Proxy-aware URL accessors on `request`.** `request.protocol`,
|
|
958
|
+
`request.secure`, `request.host`, `request.hostname`, `request.origin`,
|
|
959
|
+
`request.fullUrl`, and `request.querystring` introspect the request URL and
|
|
960
|
+
connection. They honor `X-Forwarded-Proto` / `X-Forwarded-Host` over the raw
|
|
961
|
+
URL, so an app behind a TLS-terminating proxy or load balancer sees the
|
|
962
|
+
client's real scheme and host — use `origin` to build absolute links and
|
|
963
|
+
`secure` to gate insecure requests. (Koa-inspired.)
|
|
964
|
+
- **`response.back(fallback?)` and `redirect("back")`.** Redirect to the
|
|
965
|
+
request's `Referer`, falling back to `fallback` (default `"/"`) when it's
|
|
966
|
+
absent — the "return where you came from" shortcut for post-form flows.
|
|
967
|
+
- **`response.attachment(filename?)`.** Marks the response as a downloadable
|
|
968
|
+
attachment via `Content-Disposition`, emitting both a quoted ASCII `filename`
|
|
969
|
+
and an RFC 5987 `filename*` so non-ASCII names survive. Chainable, so pair it
|
|
970
|
+
with `type()`.
|
|
971
|
+
- **Encoding & charset negotiation.** `request.encoding(encodings)` /
|
|
972
|
+
`request.encodings()` and `request.charset(charsets)` / `request.charsets()`
|
|
973
|
+
complete the content-negotiation set alongside the existing `accepts` and
|
|
974
|
+
`language` helpers, using the same q-weight and `*` rules.
|
|
975
|
+
|
|
976
|
+
## [0.61.0] — 2026-07-11
|
|
977
|
+
|
|
978
|
+
### Added
|
|
979
|
+
|
|
980
|
+
- **Batteries-included database adapters.** Ready-made `Connection` implementations
|
|
981
|
+
for the common drivers, so you no longer hand-write the `select`/`write` bridge.
|
|
982
|
+
Each ships as an optional subpath import and takes your driver instance:
|
|
983
|
+
- `@shaferllc/keel/db/d1` — `d1Connection(env.DB)` for Cloudflare D1 (sqlite).
|
|
984
|
+
- `@shaferllc/keel/db/pg` — `pgConnection(client)` for any node-postgres-compatible
|
|
985
|
+
client: `pg` on Node or `@neondatabase/serverless` on the edge (postgres).
|
|
986
|
+
- `@shaferllc/keel/db/libsql` — `libsqlConnection(client)` for `@libsql/client` /
|
|
987
|
+
Turso, on Node and the edge (sqlite).
|
|
988
|
+
|
|
989
|
+
Each adapter **duck-types its driver** (a minimal structural interface) and
|
|
990
|
+
imports no driver — so Keel's core stays dependency-free and nothing is bundled
|
|
991
|
+
until you import an adapter, and you install only the driver you use (a peer, not
|
|
992
|
+
a Keel dependency). D1 and libSQL return the last insert id natively; Postgres
|
|
993
|
+
needs a `RETURNING id` clause for `insertGetId()`.
|
|
994
|
+
|
|
995
|
+
[0.61.0]: https://github.com/shaferllc/keel/releases/tag/v0.61.0
|
|
996
|
+
|
|
997
|
+
## [0.60.0] — 2026-07-11
|
|
998
|
+
|
|
999
|
+
### Added
|
|
1000
|
+
|
|
1001
|
+
- **Multiple database connections.** The database layer grew from a single global
|
|
1002
|
+
connection to a named registry, so an app can talk to several databases at once
|
|
1003
|
+
— a Postgres primary and a SQLite/D1 cache, a separate reporting warehouse, a
|
|
1004
|
+
per-tenant shard — each with its own dialect. Inspired by the common API behind
|
|
1005
|
+
the [Feathers database adapters](https://feathersjs.com/api/databases/adapters.html)
|
|
1006
|
+
(register many, route per resource), but kept in Keel's driver-agnostic,
|
|
1007
|
+
edge-safe `Connection` model (still no bundled driver):
|
|
1008
|
+
- `addConnection(name, conn, dialect?)` registers a named connection alongside
|
|
1009
|
+
the default; `setConnection` still registers the default (unchanged).
|
|
1010
|
+
- `db(table, connectionName?)` routes a single query to a named connection.
|
|
1011
|
+
- `connection(name?)` returns a `ConnectionHandle` — `table()` plus a raw,
|
|
1012
|
+
dialect-adjusted `select`/`write` bridge.
|
|
1013
|
+
- `Model.connection` (a `static`) puts a whole model — reads, writes, and
|
|
1014
|
+
relations — on a chosen connection.
|
|
1015
|
+
- `setDefaultConnection(name)` switches the default; `connectionNames()` lists
|
|
1016
|
+
the registered ones; `clearConnections()` resets (test helper).
|
|
1017
|
+
- New type `ConnectionHandle`.
|
|
1018
|
+
|
|
1019
|
+
Fully backward compatible: `setConnection` + `db(table)` behave exactly as
|
|
1020
|
+
before (the unnamed default lives under `"default"`), and connection resolution
|
|
1021
|
+
stays lazy — building a query never throws, only running one does.
|
|
1022
|
+
|
|
1023
|
+
[0.60.0]: https://github.com/shaferllc/keel/releases/tag/v0.60.0
|
|
1024
|
+
|
|
1025
|
+
## [0.59.0] — 2026-07-11
|
|
1026
|
+
|
|
1027
|
+
### Added
|
|
1028
|
+
|
|
1029
|
+
- **Stateless token authentication (JWT + bearer guard).** Took the token half of
|
|
1030
|
+
the [Feathers authentication API](https://feathersjs.com/api/authentication/)
|
|
1031
|
+
([service](https://feathersjs.com/api/authentication/service.html),
|
|
1032
|
+
[JWT](https://feathersjs.com/api/authentication/jwt.html),
|
|
1033
|
+
[hook](https://feathersjs.com/api/authentication/hook.html)) — the piece Keel
|
|
1034
|
+
was missing next to its session/cookie `Auth` — and built it edge-native:
|
|
1035
|
+
- **`jwt`** — HS256 sign/verify on the Web Crypto API (no `jsonwebtoken`, no
|
|
1036
|
+
native bindings), signed with `config('app.key')`. `jwt.sign(payload, opts?)`
|
|
1037
|
+
stamps `iat` and (with `expiresIn`) `exp`, and supports `subject`/`issuer`/
|
|
1038
|
+
`audience`/`secret`; `jwt.verify()` returns the payload or `null` for a
|
|
1039
|
+
malformed, tampered, expired, not-yet-valid, or wrong-issuer/audience token.
|
|
1040
|
+
Only HS256 is accepted — `alg: none` and asymmetric algs are refused, closing
|
|
1041
|
+
the JWT algorithm-confusion hole. New types `JwtPayload`, `JwtSignOptions`,
|
|
1042
|
+
`JwtVerifyOptions`.
|
|
1043
|
+
- **`bearerAuth(options?)`** — a guard middleware that reads `Authorization:
|
|
1044
|
+
Bearer <token>`, verifies it, and makes the token's `sub` the authenticated
|
|
1045
|
+
id, so `auth().user()` resolves through the registered provider exactly as
|
|
1046
|
+
with sessions. Needs no session store (ideal on Workers). `{ optional: true }`
|
|
1047
|
+
lets unauthenticated requests through.
|
|
1048
|
+
- **`auth().id()`** now honors a `bearerAuth()` token (it wins over the session)
|
|
1049
|
+
and reads the request context directly, so token-only APIs work without
|
|
1050
|
+
`sessionMiddleware()`.
|
|
1051
|
+
|
|
1052
|
+
Username/password login is unchanged — `hash` + `auth().login()` already cover
|
|
1053
|
+
the local flow. OAuth remains out of scope. All additive and backward compatible.
|
|
1054
|
+
|
|
1055
|
+
[0.59.0]: https://github.com/shaferllc/keel/releases/tag/v0.59.0
|
|
1056
|
+
|
|
1057
|
+
## [0.58.0] — 2026-07-11
|
|
1058
|
+
|
|
1059
|
+
### Added
|
|
1060
|
+
|
|
1061
|
+
- **Errors: the full HTTP exception family.** Rounded out the built-in exceptions
|
|
1062
|
+
against the [Feathers errors API](https://feathersjs.com/api/errors.html) — the
|
|
1063
|
+
set now covers every common status, each with a fixed `status` and a stable
|
|
1064
|
+
machine `code`: `BadRequestException` (400), `PaymentRequiredException` (402),
|
|
1065
|
+
`MethodNotAllowedException` (405), `NotAcceptableException` (406),
|
|
1066
|
+
`RequestTimeoutException` (408), `ConflictException` (409),
|
|
1067
|
+
`LengthRequiredException` (411), `TooManyRequestsException` (429),
|
|
1068
|
+
`ServerErrorException` (500), `NotImplementedException` (501),
|
|
1069
|
+
`BadGatewayException` (502), and `ServiceUnavailableException` (503) — joining
|
|
1070
|
+
the existing `NotFoundException`, `UnauthorizedException`, `ForbiddenException`,
|
|
1071
|
+
and `ValidationException`. `STATUS_TEXT` gained labels for the new statuses.
|
|
1072
|
+
- **Structured error data.** `HttpException` gained an optional `data` bag
|
|
1073
|
+
(`new ConflictException(message, data)`) that surfaces in the JSON error body
|
|
1074
|
+
under `data`, plus a `toJSON()` returning the exact rendered body shape
|
|
1075
|
+
(`{ error, status, code?, data? }`, and `errors` for `ValidationException`) so
|
|
1076
|
+
an exception can be serialized outside the HTTP kernel.
|
|
1077
|
+
|
|
1078
|
+
All additive and backward compatible.
|
|
1079
|
+
|
|
1080
|
+
[0.58.0]: https://github.com/shaferllc/keel/releases/tag/v0.58.0
|
|
1081
|
+
|
|
1082
|
+
## [0.57.0] — 2026-07-11
|
|
1083
|
+
|
|
1084
|
+
### Added
|
|
1085
|
+
|
|
1086
|
+
- **Application object: Feathers-style ergonomics.** Adopted the useful parts of
|
|
1087
|
+
the [Feathers Application API](https://feathersjs.com/api/application.html)
|
|
1088
|
+
onto `Application`, all additive:
|
|
1089
|
+
- `app.configure(fn)` — run a `(app) => unknown` configurator and chain. The
|
|
1090
|
+
one-shot inline alternative to a `ServiceProvider` (no register/boot split).
|
|
1091
|
+
- `app.set(key, value)` / `app.get(key, fallback?)` — app-wide settings store,
|
|
1092
|
+
backed by `Config` so `app.set` and `config().get` share one store.
|
|
1093
|
+
- `app.on` / `app.once` / `app.off` / `app.emit` — app-level events delegating
|
|
1094
|
+
to the `Events` singleton (same emitter as the global `listen()` helper).
|
|
1095
|
+
- New exported type `Configurator`.
|
|
1096
|
+
|
|
1097
|
+
All backward compatible. `app.listen`/`teardown` map to Keel's existing
|
|
1098
|
+
Hono adapter + `boot()`/`terminate()`; the registry (`use`/`service`) is
|
|
1099
|
+
Keel's [container](https://keeljs.com/docs/container) + service broker.
|
|
1100
|
+
|
|
1101
|
+
[0.57.0]: https://github.com/shaferllc/keel/releases/tag/v0.57.0
|
|
1102
|
+
|
|
1103
|
+
## [0.56.0] — 2026-07-11
|
|
1104
|
+
|
|
1105
|
+
### Added
|
|
1106
|
+
|
|
1107
|
+
- **Service broker: params validation & result caching.** Six more Moleculer pages
|
|
1108
|
+
checked ([validating](https://moleculer.services/docs/0.15/validating),
|
|
1109
|
+
[caching](https://moleculer.services/docs/0.15/caching),
|
|
1110
|
+
[metrics](https://moleculer.services/docs/0.15/metrics),
|
|
1111
|
+
[tracing](https://moleculer.services/docs/0.15/tracing),
|
|
1112
|
+
[errors](https://moleculer.services/docs/0.15/errors),
|
|
1113
|
+
[runner](https://moleculer.services/docs/0.15/runner)):
|
|
1114
|
+
- **Validating** — an action's `params` schema is validated (and coerced) before
|
|
1115
|
+
the handler; a bad call rejects with `ValidationException`. Bring your own
|
|
1116
|
+
Zod-style schema.
|
|
1117
|
+
- **Caching** — mark an action `cache: true | { ttl, keys }` and give the broker
|
|
1118
|
+
a `cacher` (any Keel `Cache` — memory or Redis); results memoize by action +
|
|
1119
|
+
params (`keys` limits the key). No cacher → no-op.
|
|
1120
|
+
- **Metrics / tracing** — the middleware `localAction` seam is the hook; the
|
|
1121
|
+
trace context (`requestID`/`parentID`/`level`/`caller`) is already on every
|
|
1122
|
+
ctx. **Errors** — typed broker errors exist plus `createError`. **Runner** —
|
|
1123
|
+
`createService()` + `broker.start()` from boot. All documented rather than added.
|
|
1124
|
+
|
|
1125
|
+
All additive and backward compatible.
|
|
1126
|
+
|
|
1127
|
+
[0.56.0]: https://github.com/shaferllc/keel/releases/tag/v0.56.0
|
|
1128
|
+
|
|
1129
|
+
## [0.55.0] — 2026-07-11
|
|
1130
|
+
|
|
1131
|
+
### Added
|
|
1132
|
+
|
|
1133
|
+
- **Service broker: fault tolerance & registry introspection.** Two more Moleculer
|
|
1134
|
+
pages checked ([fault-tolerance](https://moleculer.services/docs/0.15/fault-tolerance),
|
|
1135
|
+
[registry](https://moleculer.services/docs/0.15/registry)):
|
|
1136
|
+
- **Retry** — `call(action, params, { retries: 3 })` re-runs the whole call on
|
|
1137
|
+
failure (total attempts = retries + 1); `BrokerOptions.retries` sets a default.
|
|
1138
|
+
- **Fallback** — `{ fallback: value }` or `{ fallback: (err, ctx) => value }`
|
|
1139
|
+
returns instead of throwing once every attempt (and `error` hooks) fails.
|
|
1140
|
+
Order: retry → error hooks → fallback → throw. (Timeout was already present.)
|
|
1141
|
+
- **Registry introspection** — `broker.hasAction(name)`, `listActions()`,
|
|
1142
|
+
`listServices()`, `getService(name)`.
|
|
1143
|
+
- **Networking / balancing** — clustering is the `Transporter` seam (NATS/Redis/
|
|
1144
|
+
TCP); single-node has one endpoint per action, so cross-node balancing doesn't
|
|
1145
|
+
apply — event **group** balancing already works via `emit(…, { groups })`.
|
|
1146
|
+
Documented rather than added.
|
|
1147
|
+
|
|
1148
|
+
All additive and backward compatible.
|
|
1149
|
+
|
|
1150
|
+
[0.55.0]: https://github.com/shaferllc/keel/releases/tag/v0.55.0
|
|
1151
|
+
|
|
1152
|
+
## [0.54.0] — 2026-07-11
|
|
1153
|
+
|
|
1154
|
+
### Added
|
|
1155
|
+
|
|
1156
|
+
- **Service broker: Moleculer-parity events & context.** A second parity pass over
|
|
1157
|
+
the broker, drawn from Moleculer's
|
|
1158
|
+
[events](https://moleculer.services/docs/0.15/events) and
|
|
1159
|
+
[context](https://moleculer.services/docs/0.15/context) pages:
|
|
1160
|
+
- **`broadcastLocal`** — broadcast to every listener on this node (mirrors
|
|
1161
|
+
`broadcast` until a real transporter would relay across nodes).
|
|
1162
|
+
- **Event groups & patterns** — the Events docs now spell out group-based
|
|
1163
|
+
balancing (`emit(..., { groups })`), and subscription keys gain the `?`
|
|
1164
|
+
single-char wildcard alongside `*` / `**`.
|
|
1165
|
+
- **Internal events** — the broker now emits `$broker.started`,
|
|
1166
|
+
`$broker.stopped`, and `$services.changed` (`{ service }` payload) that any
|
|
1167
|
+
service can subscribe to.
|
|
1168
|
+
- **Event context** — event handlers receive `ctx.eventName`, `ctx.eventType`
|
|
1169
|
+
(`"emit"` / `"broadcast"`), and `ctx.eventGroups`.
|
|
1170
|
+
- **Request-tree context** — every context now carries `ctx.parentID`,
|
|
1171
|
+
`ctx.level` (depth from 1), `ctx.caller` (invoking service), and `ctx.action`;
|
|
1172
|
+
`ctx.toJSON()` returns a log-safe snapshot with no functions or live refs.
|
|
1173
|
+
- **Broker middlewares** (Moleculer's
|
|
1174
|
+
[middlewares](https://moleculer.services/docs/0.15/middlewares)) — pass
|
|
1175
|
+
`middlewares: [...]` to wrap every action call and tap broker lifecycle. A
|
|
1176
|
+
middleware's `localAction(next, action)` wraps the handler (they compose,
|
|
1177
|
+
first = outermost); `started(broker)` / `stopped(broker)` run during
|
|
1178
|
+
`broker.start()` / `stop()`. (Service **lifecycle** hooks and per-service
|
|
1179
|
+
**`this.logger`** were already in place — the other two pages checked.)
|
|
1180
|
+
|
|
1181
|
+
All additive and backward compatible.
|
|
1182
|
+
|
|
1183
|
+
[0.54.0]: https://github.com/shaferllc/keel/releases/tag/v0.54.0
|
|
1184
|
+
|
|
1185
|
+
## [0.53.0] — 2026-07-11
|
|
1186
|
+
|
|
1187
|
+
### Added
|
|
1188
|
+
|
|
1189
|
+
- **Route config / metadata.** Attach arbitrary data to a route or group with
|
|
1190
|
+
`.config({ … })` and read it in the handler or route middleware via
|
|
1191
|
+
`request.route.config` — per-route flags like an auth scope, rate tier, or
|
|
1192
|
+
layout choice (Fastify's route `config`). Group config merges into every route,
|
|
1193
|
+
with a route's own keys winning. The matched-route context is now set *before*
|
|
1194
|
+
a route's middleware, so route/group middleware can branch on `request.route`.
|
|
1195
|
+
See [docs/routing.md](./docs/routing.md#route-config).
|
|
1196
|
+
|
|
1197
|
+
[0.53.0]: https://github.com/shaferllc/keel/releases/tag/v0.53.0
|
|
1198
|
+
|
|
1199
|
+
## [0.52.0] — 2026-07-11
|
|
1200
|
+
|
|
1201
|
+
### Added
|
|
1202
|
+
|
|
1203
|
+
- **Service broker: Moleculer-parity actions & services.** The
|
|
1204
|
+
[broker](./docs/broker.md) grows the pieces a service-oriented app leans on,
|
|
1205
|
+
drawn from Moleculer's [services](https://moleculer.services/docs/0.15/services)
|
|
1206
|
+
and [actions](https://moleculer.services/docs/0.15/actions) pages:
|
|
1207
|
+
- **Full action definitions** — an action may now be `{ handler, visibility,
|
|
1208
|
+
timeout, hooks }` instead of only a bare handler.
|
|
1209
|
+
- **Action hooks** — `before` / `after` / `error` at the service level (keyed by
|
|
1210
|
+
action name, with `*`, `"a|b"`, and glob matching) or inline per action, run
|
|
1211
|
+
in Moleculer's order (before: wildcard → named → action; after/error reversed).
|
|
1212
|
+
- **Visibility** — `published` / `public` / `protected` / `private`; `private`
|
|
1213
|
+
actions are hidden from `call` but reachable internally via `this.actions.x`.
|
|
1214
|
+
- **`mcall`** — batch calls as an array or keyed map, with `settled` for
|
|
1215
|
+
per-call `{ status, value | reason }`.
|
|
1216
|
+
- **Mixins** — reusable schemas merged by type (settings/metadata deep-merge,
|
|
1217
|
+
actions/events/methods/hooks by key, lifecycle hooks chained), with a
|
|
1218
|
+
`merged()` hook; the service's own schema wins on conflict.
|
|
1219
|
+
- **Dependencies** — `dependencies` gates a service's `started` hook on other
|
|
1220
|
+
services being registered; `broker.waitForServices()` / `this.waitForServices()`
|
|
1221
|
+
wait explicitly.
|
|
1222
|
+
- **Richer context** — `ctx.locals` (per-call scratch), `ctx.headers` (transient,
|
|
1223
|
+
not propagated), and `ctx.requestID` (correlation id threaded through the
|
|
1224
|
+
request tree); `metadata` on the service instance; event listeners may declare
|
|
1225
|
+
a `group` that `emit`'s `groups` option targets.
|
|
1226
|
+
|
|
1227
|
+
All additive and backward compatible — a bare-handler action, function-shorthand
|
|
1228
|
+
event, and hook-less service behave exactly as before.
|
|
1229
|
+
|
|
1230
|
+
[0.52.0]: https://github.com/shaferllc/keel/releases/tag/v0.52.0
|
|
1231
|
+
|
|
1232
|
+
## [0.51.0] — 2026-07-11
|
|
1233
|
+
|
|
1234
|
+
### Added
|
|
1235
|
+
|
|
1236
|
+
- **Response header helpers.** The `response` accessor gains `headers({...})` (set
|
|
1237
|
+
several at once), `getHeader(name)`, and `hasHeader(name)` — so middleware can
|
|
1238
|
+
inspect and conditionally set what a handler already put on the response (e.g.
|
|
1239
|
+
a default `cache-control`). Brings `response` to parity with Fastify's Reply.
|
|
1240
|
+
See [docs/request-response.md](./docs/request-response.md).
|
|
1241
|
+
- **Design principles** documented in
|
|
1242
|
+
[docs/architecture.md](./docs/architecture.md#design-principles) — edge-safe /
|
|
1243
|
+
driver-agnostic and explicit-over-implicit spelled out alongside the existing
|
|
1244
|
+
container-first and thin-over-clever tenets.
|
|
1245
|
+
|
|
1246
|
+
[0.51.0]: https://github.com/shaferllc/keel/releases/tag/v0.51.0
|
|
1247
|
+
|
|
1248
|
+
## [0.50.0] — 2026-07-11
|
|
1249
|
+
|
|
1250
|
+
### Added
|
|
1251
|
+
|
|
1252
|
+
- **Parameterized providers.** Service providers — Keel's plugin system — now take
|
|
1253
|
+
options at registration: `app.register(RateLimitProvider, { max: 100 })`, typed
|
|
1254
|
+
via `ServiceProvider<{ max: number }>` and read as `this.options`. The same
|
|
1255
|
+
provider class can register more than once with different options, so a provider
|
|
1256
|
+
is now genuinely reusable (Fastify's `register(plugin, options)`). Backward
|
|
1257
|
+
compatible — options default to `{}`. (Keel providers stay un-encapsulated by
|
|
1258
|
+
design; per-request scoping is [middleware](./docs/middleware.md).) See
|
|
1259
|
+
[docs/providers.md](./docs/providers.md#providers-are-keels-plugin-system).
|
|
1260
|
+
|
|
1261
|
+
[0.50.0]: https://github.com/shaferllc/keel/releases/tag/v0.50.0
|
|
1262
|
+
|
|
1263
|
+
## [0.49.0] — 2026-07-11
|
|
1264
|
+
|
|
1265
|
+
### Added
|
|
1266
|
+
|
|
1267
|
+
- **Broadcasting.** Push events to clients in real time over named channels on a
|
|
1268
|
+
pluggable `Broadcaster` — the core owns no socket, so point it at Pusher/Ably
|
|
1269
|
+
(`fetch`), a Cloudflare Durable Object, or the built-in `MemoryBroadcaster`
|
|
1270
|
+
(in-process fan-out, for tests). `broadcast(channels, event, payload)`
|
|
1271
|
+
publishes; `channelAuth("orders.{id}", (user, params) => …)` gates private and
|
|
1272
|
+
presence channels (return `false`/`true`/member-data), resolved by
|
|
1273
|
+
`authorizeChannel` at your socket endpoint — composing with `auth()` and
|
|
1274
|
+
authorization. `MemoryBroadcaster.subscribe()` fans out in-process (Durable
|
|
1275
|
+
Object / SSE). See [docs/broadcasting.md](./docs/broadcasting.md).
|
|
1276
|
+
|
|
1277
|
+
[0.49.0]: https://github.com/shaferllc/keel/releases/tag/v0.49.0
|
|
1278
|
+
|
|
1279
|
+
## [0.48.0] — 2026-07-11
|
|
1280
|
+
|
|
1281
|
+
### Added
|
|
1282
|
+
|
|
1283
|
+
- **Task scheduling.** Declare recurring work with a fluent cadence — `schedule(new
|
|
1284
|
+
PruneSessions()).daily()`, `schedule(() => sync()).everyFiveMinutes()`,
|
|
1285
|
+
`schedule(job).cron("0 9 * * 1")` — then run the scheduler once a minute from a
|
|
1286
|
+
single trigger. Cadences: `everyMinute` … `everyThirtyMinutes`, `hourly` /
|
|
1287
|
+
`hourlyAt`, `daily` / `dailyAt("13:30")`, `weekly` / `monthly`, or any 5-field
|
|
1288
|
+
`cron()`. `scheduler().runDue(now)` runs everything due (to the minute); `due()`
|
|
1289
|
+
lists without running. Built-in cron matcher (`*`, lists, ranges, steps, and
|
|
1290
|
+
standard dom/dow semantics) — wire it to Cloudflare Cron Triggers'
|
|
1291
|
+
`scheduled()` handler or a Node interval. A task is a `Job` or a function. See
|
|
1292
|
+
[docs/scheduling.md](./docs/scheduling.md).
|
|
1293
|
+
|
|
1294
|
+
[0.48.0]: https://github.com/shaferllc/keel/releases/tag/v0.48.0
|
|
1295
|
+
|
|
1296
|
+
## [0.47.0] — 2026-07-11
|
|
1297
|
+
|
|
1298
|
+
### Added
|
|
1299
|
+
|
|
1300
|
+
- **File storage.** A driver-agnostic storage layer on a pluggable `Disk` — the
|
|
1301
|
+
core imports no filesystem or SDK, so it runs on Node and the edge.
|
|
1302
|
+
`setDisk(disk, name?)` then `storage(name?)`: `put` (string / bytes /
|
|
1303
|
+
ArrayBuffer) / `get` / `getText` / `exists` / `delete` / `list(prefix?)` /
|
|
1304
|
+
`url`. `MemoryDisk` is a full in-memory driver and the default, so `storage()`
|
|
1305
|
+
works in tests with no setup; point disks at the local filesystem (Node), S3
|
|
1306
|
+
(`fetch`), or a Cloudflare R2 binding (adapters in the docs). Register several
|
|
1307
|
+
disks by name and select with `storage("s3")`. See
|
|
1308
|
+
[docs/storage.md](./docs/storage.md).
|
|
1309
|
+
|
|
1310
|
+
[0.47.0]: https://github.com/shaferllc/keel/releases/tag/v0.47.0
|
|
1311
|
+
|
|
1312
|
+
## [0.46.0] — 2026-07-11
|
|
1313
|
+
|
|
1314
|
+
### Added
|
|
1315
|
+
|
|
1316
|
+
- **Authorization — gates & policies.** Where `auth()` is *who you are*, this is
|
|
1317
|
+
*what you're allowed to do*. `define(ability, (user, ...args) => …)` registers a
|
|
1318
|
+
gate; `policy(Model, PolicyClass)` groups abilities as methods on a plain class,
|
|
1319
|
+
and `can("update", post)` routes to `PostPolicy.update(user, post)` by the
|
|
1320
|
+
argument's class. `can` / `cannot` return booleans; `authorize` throws a `403`;
|
|
1321
|
+
`canFor` / `authorizeFor` check a specific user; `gateBefore` short-circuits
|
|
1322
|
+
every check (admin bypass). The current user resolves from `auth().user()` by
|
|
1323
|
+
default (overridable with `setUserResolver`); unknown abilities deny. See
|
|
1324
|
+
[docs/authorization.md](./docs/authorization.md).
|
|
1325
|
+
|
|
1326
|
+
[0.46.0]: https://github.com/shaferllc/keel/releases/tag/v0.46.0
|
|
1327
|
+
|
|
1328
|
+
## [0.45.0] — 2026-07-11
|
|
1329
|
+
|
|
1330
|
+
### Added
|
|
1331
|
+
|
|
1332
|
+
- **Test client.** `testClient(app)` injects requests into your app — no server,
|
|
1333
|
+
no port — and returns a `TestResponse` with verb helpers (`get` / `post` (JSON
|
|
1334
|
+
body) / `put` / `patch` / `delete`) and fluent, chainable assertions
|
|
1335
|
+
(`assertStatus` / `assertOk` / `assertJson` / `assertText` / `assertHeader` /
|
|
1336
|
+
`assertRedirect`). The response body is pre-buffered, so reads are synchronous
|
|
1337
|
+
and repeatable. Accepts an `Application`, an `HttpKernel` (to register global
|
|
1338
|
+
middleware first), or any `request()`-able. Edge-safe — the same fetch-style
|
|
1339
|
+
injection Keel's own suite uses, minus the boilerplate. See
|
|
1340
|
+
[docs/testing.md](./docs/testing.md).
|
|
1341
|
+
|
|
1342
|
+
[0.45.0]: https://github.com/shaferllc/keel/releases/tag/v0.45.0
|
|
1343
|
+
|
|
1344
|
+
## [0.44.0] — 2026-07-11
|
|
1345
|
+
|
|
1346
|
+
### Added
|
|
1347
|
+
|
|
1348
|
+
- **Declarative request validation.** `validateRequest({ body, query, params })`
|
|
1349
|
+
is middleware that validates the request *before* the handler runs — rejecting
|
|
1350
|
+
a bad request with a `422` `ValidationException` (errors from every part
|
|
1351
|
+
aggregated, keyed `body.field` / `query.field` / `params.field`) so the handler
|
|
1352
|
+
only ever sees valid input. `validated(part)` returns the parsed, typed value.
|
|
1353
|
+
The declarative counterpart to Fastify's route schemas, built on the same
|
|
1354
|
+
schema-agnostic `validate()` engine (bring your own Zod-style schema). See
|
|
1355
|
+
[docs/validation.md](./docs/validation.md#declarative-validation-before-the-handler).
|
|
1356
|
+
|
|
1357
|
+
[0.44.0]: https://github.com/shaferllc/keel/releases/tag/v0.44.0
|
|
1358
|
+
|
|
1359
|
+
## [0.43.0] — 2026-07-10
|
|
1360
|
+
|
|
1361
|
+
### Added
|
|
1362
|
+
|
|
1363
|
+
- **Per-request logging.** `requestLogger()` middleware binds a child logger with
|
|
1364
|
+
a generated `reqId` to each request, so every log line within a request
|
|
1365
|
+
correlates (Fastify's `request.log`). It logs request start/completion
|
|
1366
|
+
(method, path, status, ms) by default; options for `genReqId`, reusing an
|
|
1367
|
+
incoming `idHeader` (distributed tracing), and disabling the auto lines.
|
|
1368
|
+
`requestLog()` reaches the current request's logger anywhere (falls back to the
|
|
1369
|
+
base logger outside a request).
|
|
1370
|
+
- **Log redaction.** `new Logger({ redact: ["password", "req.headers.authorization"] })`
|
|
1371
|
+
replaces matched values (top-level keys or dot paths) with `"[redacted]"`
|
|
1372
|
+
without mutating the logged object; inherited by child loggers. See
|
|
1373
|
+
[docs/logger.md](./docs/logger.md#per-request-logging).
|
|
1374
|
+
|
|
1375
|
+
[0.43.0]: https://github.com/shaferllc/keel/releases/tag/v0.43.0
|
|
1376
|
+
|
|
1377
|
+
## [0.42.0] — 2026-07-10
|
|
1378
|
+
|
|
1379
|
+
### Added
|
|
1380
|
+
|
|
1381
|
+
- **Application lifecycle hooks & graceful shutdown.** `onReady(hook)` runs after
|
|
1382
|
+
boot (or immediately if already booted); `onShutdown(hook)` registers cleanup
|
|
1383
|
+
and `terminate()` runs every shutdown hook newest-first (LIFO) — close DB/Redis
|
|
1384
|
+
connections, flush queues on `SIGTERM`. `terminate()` is idempotent and a
|
|
1385
|
+
throwing hook can't strand the rest (first error re-thrown after all run).
|
|
1386
|
+
`Router.onRoute(hook)` observes route registration (fired live and replayed for
|
|
1387
|
+
existing routes). Available as `Application` methods and global helpers.
|
|
1388
|
+
Request-lifecycle hooks remain [middleware](./docs/middleware.md). See
|
|
1389
|
+
[docs/hooks.md](./docs/hooks.md).
|
|
1390
|
+
|
|
1391
|
+
[0.42.0]: https://github.com/shaferllc/keel/releases/tag/v0.42.0
|
|
1392
|
+
|
|
1393
|
+
## [0.41.1] — 2026-07-10
|
|
1394
|
+
|
|
1395
|
+
### Added
|
|
1396
|
+
|
|
1397
|
+
- **Coded errors — `createError`.** Mint a reusable, coded `HttpException`
|
|
1398
|
+
subclass in one line: `createError("E_FUNDS", "Balance too low: need %s", 402)`.
|
|
1399
|
+
`%s` placeholders fill from the constructor arguments, the result renders
|
|
1400
|
+
through the default path (with `code` in the JSON body) and passes
|
|
1401
|
+
`instanceof HttpException`. The built-in exceptions now carry stable codes too
|
|
1402
|
+
(`E_NOT_FOUND`, `E_UNAUTHORIZED`, `E_FORBIDDEN`, `E_VALIDATION`), so `code`
|
|
1403
|
+
surfaces without any work. Inspired by Fastify's `@fastify/error`. Also
|
|
1404
|
+
documented: serving over **HTTP/2** needs no framework code — it's a transport
|
|
1405
|
+
concern handled by the edge platform, a reverse proxy, or a `@hono/node-server`
|
|
1406
|
+
`node:http2` option. See [docs/errors.md](./docs/errors.md#coded-errors-with-createerror)
|
|
1407
|
+
and [docs/hono.md](./docs/hono.md#serving-over-http2).
|
|
1408
|
+
|
|
1409
|
+
[0.41.1]: https://github.com/shaferllc/keel/releases/tag/v0.41.1
|
|
1410
|
+
|
|
1411
|
+
## [0.41.0] — 2026-07-10
|
|
1412
|
+
|
|
1413
|
+
### Added
|
|
1414
|
+
|
|
1415
|
+
- **Service Broker.** A Moleculer-style backbone for service-oriented code.
|
|
1416
|
+
Register services (a name plus `actions` and `events`) with a `Broker`, then
|
|
1417
|
+
reach them by string name: `broker().call("users.get", { id })` runs an action;
|
|
1418
|
+
`broker().emit("user.created", user)` fans an event out to every listener
|
|
1419
|
+
(balanced), or `broadcast` to all. Actions receive a `Context` and call other
|
|
1420
|
+
actions via `ctx.call`, threading `meta` (auth, trace ids) down through nested
|
|
1421
|
+
calls. Services support `version` prefixes (`v2.users.*`), `settings`, bound
|
|
1422
|
+
`methods`, glob event subscriptions (`user.*` / `user.**`), lifecycle hooks
|
|
1423
|
+
(`created` / `started` / `stopped`), and per-call `timeout`. Clustering lives
|
|
1424
|
+
behind a pluggable `Transporter` seam — the default `LocalTransporter` is a
|
|
1425
|
+
single-node no-op, so the core imports no network client and stays edge-safe.
|
|
1426
|
+
`broker()` / `setBroker()` manage the default instance, mirroring
|
|
1427
|
+
`redis()` / `setRedis()`. See [docs/broker.md](./docs/broker.md).
|
|
1428
|
+
|
|
1429
|
+
[0.41.0]: https://github.com/shaferllc/keel/releases/tag/v0.41.0
|
|
1430
|
+
|
|
1431
|
+
## [0.40.1] — 2026-07-10
|
|
1432
|
+
|
|
1433
|
+
### Added
|
|
1434
|
+
|
|
1435
|
+
- **Raw request-body accessors.** `request.text()`, `request.arrayBuffer()`, and
|
|
1436
|
+
`request.blob()` read the body for content types `json()` / `all()` don't
|
|
1437
|
+
handle — XML, CSV, protobuf, msgpack, or any custom format — which you then
|
|
1438
|
+
parse yourself. Keel keeps body parsing explicit (no Fastify-style content-type
|
|
1439
|
+
parser registry): you call the accessor you want. See
|
|
1440
|
+
[docs/request-response.md](./docs/request-response.md#other-content-types).
|
|
1441
|
+
|
|
1442
|
+
[0.40.1]: https://github.com/shaferllc/keel/releases/tag/v0.40.1
|
|
1443
|
+
|
|
1444
|
+
## [0.40.0] — 2026-07-10
|
|
1445
|
+
|
|
1446
|
+
### Added
|
|
1447
|
+
|
|
1448
|
+
- **Request decorators.** Attach named, computed values to the current request
|
|
1449
|
+
— `request.user` / `tenant` / `locale` — resolved lazily and memoized for the
|
|
1450
|
+
life of the request. `decorateRequest(name, resolver)` registers a resolver
|
|
1451
|
+
(sync or async), `decorated(name)` reads it (computed once, then cached),
|
|
1452
|
+
`setRequestValue(name, value)` sets it imperatively (e.g. from middleware), and
|
|
1453
|
+
`hasRequestDecorator(name)` checks. Inspired by Fastify's `decorateRequest`,
|
|
1454
|
+
but without the null-placeholder/`onRequest`-hook dance — the per-request memo
|
|
1455
|
+
is keyed off the context via a WeakMap, so nothing leaks between requests.
|
|
1456
|
+
(Decorating the *app* is already the container's job.) See
|
|
1457
|
+
[docs/decorators.md](./docs/decorators.md).
|
|
1458
|
+
|
|
1459
|
+
[0.40.0]: https://github.com/shaferllc/keel/releases/tag/v0.40.0
|
|
1460
|
+
|
|
1461
|
+
## [0.39.0] — 2026-07-10
|
|
1462
|
+
|
|
1463
|
+
### Added
|
|
1464
|
+
|
|
1465
|
+
- **Redis.** A Redis integration on a pluggable `RedisConnection` driver — the
|
|
1466
|
+
core imports no client, so it runs on Node and the edge. `setRedis(driver)`
|
|
1467
|
+
then `redis()`: `get` / `set` (with `{ ex }` / `{ px }` TTL) / `del` / `exists`
|
|
1468
|
+
/ `incr` / `decr` / `expire` / `ttl` / `keys` / `flushAll`, plus `getJson` /
|
|
1469
|
+
`setJson` and a `remember` read-through cache. `MemoryRedis` is a full
|
|
1470
|
+
in-memory driver (TTL-aware) and the default, so `redis()` works in tests with
|
|
1471
|
+
no setup; point it at Upstash (`fetch`), ioredis, or node-redis in production.
|
|
1472
|
+
`redisStore()` adapts it into a `CacheStore` so the cache can be Redis-backed.
|
|
1473
|
+
See [docs/redis.md](./docs/redis.md).
|
|
1474
|
+
|
|
1475
|
+
[0.39.0]: https://github.com/shaferllc/keel/releases/tag/v0.39.0
|
|
1476
|
+
|
|
1477
|
+
## [0.38.0] — 2026-07-10
|
|
1478
|
+
|
|
1479
|
+
### Added
|
|
1480
|
+
|
|
1481
|
+
- **ORM maturity — timestamps.** `static timestamps = true` auto-manages
|
|
1482
|
+
`created_at` / `updated_at` (both on insert, only `updated_at` on update);
|
|
1483
|
+
column names are overridable via `createdAtColumn` / `updatedAtColumn`.
|
|
1484
|
+
- **Pagination.** `Model.paginate(page, perPage)` and `db(table).paginate(...)`
|
|
1485
|
+
return a `Paginated<T>` — `{ data, total, perPage, currentPage, lastPage }`.
|
|
1486
|
+
- **Aggregates & single values.** Query builder `sum` / `avg` / `min` / `max`,
|
|
1487
|
+
plus `value(column)` (one column of the first row) and `pluck(column)` (a
|
|
1488
|
+
column across all rows).
|
|
1489
|
+
- **More query clauses.** `whereBetween`, `whereNotIn`, `whereLike`, and
|
|
1490
|
+
`latest()` / `oldest()` ordering by a timestamp column.
|
|
1491
|
+
- **Find-or-create & convenience writes.** `Model.firstOrCreate(match, values)`,
|
|
1492
|
+
`Model.updateOrCreate(match, values)`, instance `update(attrs)` (fill + save),
|
|
1493
|
+
and `refresh()` (re-read the row). See [docs/models.md](./docs/models.md).
|
|
1494
|
+
- **Full Vite support.** A first-class frontend build, the way modern full-stack
|
|
1495
|
+
frameworks do it. A `keelVite()` plugin (new `@shaferllc/keel/vite` entry) wires
|
|
1496
|
+
`vite.config.ts` — manifest, output, entrypoints, `base` — and writes a
|
|
1497
|
+
`public/hot` marker while the dev server runs; optional `reload` globs
|
|
1498
|
+
full-reload the browser on server-view changes. The `Vite` service renders the
|
|
1499
|
+
`<script>`/`<link>` tags for your entrypoints and resolves asset URLs, flipping
|
|
1500
|
+
automatically between the dev server (with HMR) and the hashed, preloaded
|
|
1501
|
+
production manifest. Helpers `viteTags` / `viteAsset` / `viteReactRefresh` slot
|
|
1502
|
+
straight into a JSX `<head>`; `scriptAttributes` / `styleAttributes`, a CDN
|
|
1503
|
+
`assetsUrl`, React Fast Refresh, and an edge-safe `useManifest` path are all
|
|
1504
|
+
covered. Tag generation is pure and runs on the edge. See
|
|
1505
|
+
[docs/vite.md](./docs/vite.md).
|
|
1506
|
+
|
|
1507
|
+
[0.38.0]: https://github.com/shaferllc/keel/releases/tag/v0.38.0
|
|
1508
|
+
|
|
1509
|
+
## [0.37.1] — 2026-07-10
|
|
1510
|
+
|
|
1511
|
+
### Fixed
|
|
1512
|
+
|
|
1513
|
+
- **`make:*` stubs import the resolvable specifier.** Generated files now import
|
|
1514
|
+
from `@shaferllc/keel/core` (the published entry point) instead of the internal
|
|
1515
|
+
`@keel/core` alias, so scaffolded code compiles in a real project.
|
|
1516
|
+
- **`Connection.select` is no longer generic** — it returns `Promise<Row[]>`, so a
|
|
1517
|
+
driver implementation no longer needs an `as Connection` cast. `db<T>()` still
|
|
1518
|
+
types results (the builder casts internally).
|
|
1519
|
+
- **`hash.verify` never throws.** A malformed hash (right prefix but a non-numeric
|
|
1520
|
+
iteration count or invalid base64) now returns `false` instead of throwing.
|
|
1521
|
+
- **Sessions handle non-Latin1 values.** Cookie serialization is UTF-8-safe, so
|
|
1522
|
+
storing emoji or non-Latin text no longer crashes the response (`btoa` throw).
|
|
1523
|
+
- **`router.url()` fills repeated params.** A `:param` appearing more than once in
|
|
1524
|
+
a path is now fully substituted, and won't match inside a longer param name.
|
|
1525
|
+
|
|
1526
|
+
[0.37.1]: https://github.com/shaferllc/keel/releases/tag/v0.37.1
|
|
1527
|
+
|
|
1528
|
+
## [0.37.0] — 2026-07-10
|
|
1529
|
+
|
|
1530
|
+
### Added
|
|
1531
|
+
|
|
1532
|
+
- **Transformers.** A presentation layer between your models and your JSON:
|
|
1533
|
+
subclass `Transformer<T>`, define one `transform()`, and get `item` /
|
|
1534
|
+
`collection` / `document`. `when(condition, value)` includes a field only when
|
|
1535
|
+
a condition holds — omitting the key entirely rather than leaking `null` — with
|
|
1536
|
+
a `mergeWhen` counterpart for groups of fields and thunks for deferred values.
|
|
1537
|
+
`whenLoaded(model, name, transformer)` embeds a relation only if it was
|
|
1538
|
+
eager-loaded, so a transformer never fires a surprise query. `document()` wraps
|
|
1539
|
+
the payload under a key (`data` by default) with top-level `meta`. Edge-safe;
|
|
1540
|
+
depends on nothing but the value you hand it. New generator
|
|
1541
|
+
`keel make:transformer`. See [docs/transformers.md](./docs/transformers.md).
|
|
1542
|
+
- **Templates.** A string templating engine:
|
|
1543
|
+
`{{ }}` / `{{{ }}}` interpolation, `{{-- comments --}}`, and `@`-tags —
|
|
1544
|
+
`@if` / `@elseif` / `@else`, `@each` (with `$loop`), `@include` / `@includeIf`,
|
|
1545
|
+
`@set`, layouts (`@layout` / `@section` / `@yield`), components with slots
|
|
1546
|
+
(`@component` / `@slot`), filters (`{{ name | upper }}`), globals, and `@dump`.
|
|
1547
|
+
`templates().register(name, src)` then `render(name, state)`. Unlike engines
|
|
1548
|
+
that compile to a function, Keel *interprets* templates against a safe
|
|
1549
|
+
expression evaluator instead of `eval` / `new Function`, so the same templates
|
|
1550
|
+
run on Node and on Workers.
|
|
1551
|
+
See [docs/templates.md](./docs/templates.md).
|
|
1552
|
+
|
|
1553
|
+
[0.37.0]: https://github.com/shaferllc/keel/releases/tag/v0.37.0
|
|
1554
|
+
|
|
1555
|
+
## [0.36.0] — 2026-07-10
|
|
1556
|
+
|
|
1557
|
+
### Added
|
|
1558
|
+
|
|
1559
|
+
- **Notifications.** Send a message to one or many recipients over pluggable
|
|
1560
|
+
channels: `notify(user, new InvoicePaid(4200))`. A `Notification` declares
|
|
1561
|
+
`via()` (channels) and per-channel content (`toMail`, `toArray`). Built-in
|
|
1562
|
+
channels: `MailChannel` (via the mailer, routed by `email` or
|
|
1563
|
+
`routeNotificationFor`), `DatabaseChannel` (inserts `toArray` into a table),
|
|
1564
|
+
and `ArrayChannel` (for tests). Set `shouldQueue = true` to deliver from a
|
|
1565
|
+
queued job. This is where the mail and queue layers compose — a custom channel
|
|
1566
|
+
is one `send` method. New generator `keel make:notification`. See
|
|
1567
|
+
[docs/notifications.md](./docs/notifications.md).
|
|
1568
|
+
|
|
1569
|
+
[0.36.0]: https://github.com/shaferllc/keel/releases/tag/v0.36.0
|
|
1570
|
+
|
|
1571
|
+
## [0.35.0] — 2026-07-10
|
|
1572
|
+
|
|
1573
|
+
### Added
|
|
1574
|
+
|
|
1575
|
+
- **Queues & jobs.** Move slow work off the request path: `dispatch(new
|
|
1576
|
+
SendWelcome(id))` places a `Job` (or a plain function) on a queue, and a
|
|
1577
|
+
pluggable `QueueDriver` decides when it runs. Built-in drivers: `SyncDriver`
|
|
1578
|
+
(runs immediately — the default), `MemoryDriver` (defers; `work()` drains it
|
|
1579
|
+
FIFO, inspect `.jobs`). `dispatch` takes `{ delay, queue }` options; a custom
|
|
1580
|
+
`push`-only driver is the seam for a real broker (e.g. Cloudflare Queues).
|
|
1581
|
+
New generator `keel make:job`. Core imports no broker, edge-safe. See
|
|
1582
|
+
[docs/queues.md](./docs/queues.md).
|
|
1583
|
+
|
|
1584
|
+
[0.35.0]: https://github.com/shaferllc/keel/releases/tag/v0.35.0
|
|
1585
|
+
|
|
1586
|
+
## [0.34.0] — 2026-07-10
|
|
1587
|
+
|
|
1588
|
+
### Added
|
|
1589
|
+
|
|
1590
|
+
- **Mail.** A fluent, edge-safe mailer: `mail().to().subject().html().send()`,
|
|
1591
|
+
with a pluggable `Transport` (like the database `Connection`). Register a
|
|
1592
|
+
default with `setMailer(transport, { from })`. Built-in transports:
|
|
1593
|
+
`ArrayTransport` (collects to `.sent`, the default and ideal for tests),
|
|
1594
|
+
`LogTransport` (logs instead of delivering), and `fetchTransport({ url,
|
|
1595
|
+
headers, body })` for provider HTTP APIs (Resend/Postmark/Mailgun) over
|
|
1596
|
+
`fetch` — the core imports no SDK. `send()` validates recipient/subject/body/
|
|
1597
|
+
from. See [docs/mail.md](./docs/mail.md).
|
|
1598
|
+
|
|
1599
|
+
[0.34.0]: https://github.com/shaferllc/keel/releases/tag/v0.34.0
|
|
1600
|
+
|
|
1601
|
+
## [0.33.0] — 2026-07-10
|
|
1602
|
+
|
|
1603
|
+
### Added
|
|
1604
|
+
|
|
1605
|
+
- **Model attribute casts.** `static casts = { active: "boolean", meta: "json",
|
|
1606
|
+
joined_at: "date" }` round-trips columns as real JS types — cast when read
|
|
1607
|
+
(from the database or `fill`) and back to storable primitives on write. This
|
|
1608
|
+
is what lets `boolean`/`json` columns bind cleanly on drivers that reject JS
|
|
1609
|
+
booleans and objects. Types: `int`, `float`, `boolean`, `string`, `json` /
|
|
1610
|
+
`array`, `date`.
|
|
1611
|
+
- **Mass-assignment guarding.** `static fillable` (allowlist) or `static
|
|
1612
|
+
guarded` (denylist) filter the attributes `create()` and `fill()` accept, so
|
|
1613
|
+
untrusted request data can't over-post protected columns. `forceFill()`
|
|
1614
|
+
bypasses it deliberately. With neither declared, behavior is unchanged
|
|
1615
|
+
(backward compatible). See [docs/models.md](./docs/models.md#attribute-casts).
|
|
1616
|
+
|
|
1617
|
+
[0.33.0]: https://github.com/shaferllc/keel/releases/tag/v0.33.0
|
|
1618
|
+
|
|
1619
|
+
## [0.32.0] — 2026-07-10
|
|
1620
|
+
|
|
1621
|
+
### Added
|
|
1622
|
+
|
|
1623
|
+
- **Factories & seeders.** `factory(Model, (f, i) => ({ ... }))` builds model
|
|
1624
|
+
attributes with a built-in, dependency-free `Faker` (names, emails, words,
|
|
1625
|
+
numbers, uuids — seedable for deterministic runs). Call `.make()` (unsaved) or
|
|
1626
|
+
`.create()` (persisted), `.count(n)` for batches, and override attributes
|
|
1627
|
+
inline. `Seeder` classes have a `run()` and can `call([OtherSeeder])` to
|
|
1628
|
+
compose; `seed(DatabaseSeeder)` runs one. New generators `keel make:factory`
|
|
1629
|
+
and `keel make:seeder`. Edge-safe (no external faker library). See
|
|
1630
|
+
[docs/factories.md](./docs/factories.md).
|
|
1631
|
+
|
|
1632
|
+
[0.32.0]: https://github.com/shaferllc/keel/releases/tag/v0.32.0
|
|
1633
|
+
|
|
1634
|
+
## [0.31.0] — 2026-07-10
|
|
1635
|
+
|
|
1636
|
+
### Added
|
|
1637
|
+
|
|
1638
|
+
- **Model relationships.** Define relationships as methods on your model:
|
|
1639
|
+
`hasMany` / `hasOne` / `belongsTo` / `belongsToMany`, with conventional
|
|
1640
|
+
foreign keys (`user_id`) you can override. Relations are awaitable
|
|
1641
|
+
(`await user.posts()`), expose `.query()` to drop to the builder, and
|
|
1642
|
+
`Model.load(models, "posts", "roles")` eager-loads with one `whereIn` per
|
|
1643
|
+
relation (fixes N+1). `belongsToMany` reads through a pivot table and offers
|
|
1644
|
+
`attach` / `detach` / `sync`. Loaded relations stay out of `save()` and
|
|
1645
|
+
serialize through `toJSON()`. Runs entirely on the query builder — no JOINs,
|
|
1646
|
+
edge-safe. See [docs/models.md](./docs/models.md#relationships).
|
|
1647
|
+
|
|
1648
|
+
[0.31.0]: https://github.com/shaferllc/keel/releases/tag/v0.31.0
|
|
1649
|
+
|
|
1650
|
+
## [0.30.0] — 2026-07-10
|
|
1651
|
+
|
|
1652
|
+
### Added
|
|
1653
|
+
|
|
1654
|
+
- **Migrations.** A fluent schema builder (`schema.createTable(name, t => { t.id();
|
|
1655
|
+
t.string("email").unique(); t.timestamps(); })`) and a `Migrator` that runs
|
|
1656
|
+
`{ name, up, down }` migrations against your connection, tracking applied ones
|
|
1657
|
+
in a `migrations` table (`up`/`down`/`ran`, batched). Dialect-aware SQL
|
|
1658
|
+
(sqlite/mysql/postgres). See [docs/migrations.md](./docs/migrations.md).
|
|
1659
|
+
|
|
1660
|
+
[0.30.0]: https://github.com/shaferllc/keel/releases/tag/v0.30.0
|
|
1661
|
+
|
|
1662
|
+
## [0.29.0] — 2026-07-10
|
|
1663
|
+
|
|
1664
|
+
### Added
|
|
1665
|
+
|
|
1666
|
+
- **Active-record `Model`.** Subclass `Model`, set a `table`, and get static
|
|
1667
|
+
`find` / `findOrFail` / `all` / `first` / `where` / `create` plus instance
|
|
1668
|
+
`save` (insert or update), `delete`, `fill`, and `toJSON`. Built on the query
|
|
1669
|
+
builder, so it runs on any registered connection (edge-safe). `Model.query()`
|
|
1670
|
+
drops to the raw builder for richer queries. See [docs/models.md](./docs/models.md).
|
|
1671
|
+
|
|
1672
|
+
[0.29.0]: https://github.com/shaferllc/keel/releases/tag/v0.29.0
|
|
1673
|
+
|
|
1674
|
+
## [0.28.0] — 2026-07-10
|
|
1675
|
+
|
|
1676
|
+
### Added
|
|
1677
|
+
|
|
1678
|
+
- **Database query builder.** A driver-agnostic, parameterized query builder:
|
|
1679
|
+
`db(table).where().orderBy().limit().get()/first()/count()/exists()`, plus
|
|
1680
|
+
`whereIn` / `whereNull` / `orWhere`, and `insert` / `insertGetId` / `update` /
|
|
1681
|
+
`delete`. Runs through a two-method `Connection` you register with
|
|
1682
|
+
`setConnection(conn, dialect)` — works with D1, Neon/Postgres, PlanetScale,
|
|
1683
|
+
Turso, better-sqlite3, `pg`. The core imports no driver (edge-safe). See
|
|
1684
|
+
[docs/database.md](./docs/database.md).
|
|
1685
|
+
|
|
1686
|
+
[0.28.0]: https://github.com/shaferllc/keel/releases/tag/v0.28.0
|
|
1687
|
+
|
|
1688
|
+
## [0.27.0] — 2026-07-10
|
|
1689
|
+
|
|
1690
|
+
### Added
|
|
1691
|
+
|
|
1692
|
+
- **Authentication.** Session-based auth: `auth().login(id)` / `logout()` /
|
|
1693
|
+
`check()` / `guest()` / `id()` / `user()`, a pluggable user provider via
|
|
1694
|
+
`setUserProvider()`, and an `authGuard({ redirectTo? })` middleware (401 or
|
|
1695
|
+
redirect). Built on the session + hash primitives. See
|
|
1696
|
+
[docs/authentication.md](./docs/authentication.md).
|
|
1697
|
+
|
|
1698
|
+
[0.27.0]: https://github.com/shaferllc/keel/releases/tag/v0.27.0
|
|
1699
|
+
|
|
1700
|
+
## [0.26.0] — 2026-07-10
|
|
1701
|
+
|
|
1702
|
+
### Added
|
|
1703
|
+
|
|
1704
|
+
- **Logger.** A leveled logger (`logger().debug/info/warn/error`) with structured
|
|
1705
|
+
JSON output (pretty in debug), a level threshold from `config('logger.level')`,
|
|
1706
|
+
and `logger().child({ … })` for bound fields. See [docs/logger.md](./docs/logger.md).
|
|
1707
|
+
|
|
1708
|
+
[0.26.0]: https://github.com/shaferllc/keel/releases/tag/v0.26.0
|
|
1709
|
+
|
|
1710
|
+
## [0.25.0] — 2026-07-10
|
|
1711
|
+
|
|
1712
|
+
### Added
|
|
1713
|
+
|
|
1714
|
+
- **Rate limiting.** `rateLimiter({ max, window, key, message })` — a fixed-window
|
|
1715
|
+
limiter middleware with per-key buckets (client IP by default), the standard
|
|
1716
|
+
`X-RateLimit-*` / `Retry-After` headers, and `429` on exceed. In-memory store
|
|
1717
|
+
(pluggable for distributed limiting). See [docs/rate-limiting.md](./docs/rate-limiting.md).
|
|
1718
|
+
|
|
1719
|
+
[0.25.0]: https://github.com/shaferllc/keel/releases/tag/v0.25.0
|
|
1720
|
+
|
|
1721
|
+
## [0.24.0] — 2026-07-10
|
|
1722
|
+
|
|
1723
|
+
### Added
|
|
1724
|
+
|
|
1725
|
+
- **Password hashing.** `hash.make(password)` (PBKDF2-SHA256, self-describing),
|
|
1726
|
+
`hash.verify(hashed, password)` (timing-safe), and `hash.needsRehash()`.
|
|
1727
|
+
- **Value encryption.** `encryption.encrypt(value)` / `encryption.decrypt(token)`
|
|
1728
|
+
(AES-GCM, keyed by `config('app.key')`; `decrypt` returns `null` on tamper).
|
|
1729
|
+
- Both use the Web Crypto API — edge-safe, no native bindings. See
|
|
1730
|
+
[docs/hashing.md](./docs/hashing.md).
|
|
1731
|
+
|
|
1732
|
+
[0.24.0]: https://github.com/shaferllc/keel/releases/tag/v0.24.0
|
|
1733
|
+
|
|
1734
|
+
## [0.23.0] — 2026-07-10
|
|
1735
|
+
|
|
1736
|
+
### Added
|
|
1737
|
+
|
|
1738
|
+
- **Debugging helpers.** `dump(...values)` prints to the console and returns its
|
|
1739
|
+
first argument (inline-friendly); `dd(...values)` dumps to the browser and
|
|
1740
|
+
halts the request via a self-rendering exception. Both edge-safe. See
|
|
1741
|
+
[docs/debugging.md](./docs/debugging.md).
|
|
1742
|
+
|
|
1743
|
+
[0.23.0]: https://github.com/shaferllc/keel/releases/tag/v0.23.0
|
|
1744
|
+
|
|
1745
|
+
## [0.22.0] — 2026-07-10
|
|
1746
|
+
|
|
1747
|
+
### Added
|
|
1748
|
+
|
|
1749
|
+
- **Self-handling & reportable exceptions.** An exception with a `handle(c)`
|
|
1750
|
+
method renders itself; one with a `report()` method has it called (and
|
|
1751
|
+
awaited) before rendering — for logging/metrics, without masking the error.
|
|
1752
|
+
- **Error codes.** `HttpException` now carries an optional `code` (e.g.
|
|
1753
|
+
`E_UNAUTHORIZED`), included in the JSON error body. See
|
|
1754
|
+
[docs/errors.md](./docs/errors.md).
|
|
1755
|
+
|
|
1756
|
+
[0.22.0]: https://github.com/shaferllc/keel/releases/tag/v0.22.0
|
|
1757
|
+
|
|
1758
|
+
## [0.21.0] — 2026-07-10
|
|
1759
|
+
|
|
1760
|
+
### Added
|
|
1761
|
+
|
|
1762
|
+
- **URL builder.** `router.url(name, params, { qs })` now takes a query string.
|
|
1763
|
+
- **Signed URLs.** `router.signedUrl(name, params, { qs, expiresIn })` produces a
|
|
1764
|
+
tamper-proof link (HMAC-SHA256 via Web Crypto, keyed by `config('app.key')`);
|
|
1765
|
+
`router.hasValidSignature()` verifies the current request. Edge-safe. See
|
|
1766
|
+
[docs/url-builder.md](./docs/url-builder.md).
|
|
1767
|
+
|
|
1768
|
+
[0.21.0]: https://github.com/shaferllc/keel/releases/tag/v0.21.0
|
|
1769
|
+
|
|
1770
|
+
## [0.20.0] — 2026-07-10
|
|
1771
|
+
|
|
1772
|
+
### Added
|
|
1773
|
+
|
|
1774
|
+
- **Named middleware registry.** `router.named({ auth, admin })` registers
|
|
1775
|
+
middleware by name; reference it with `.use("auth")` / `.middleware([...])` on
|
|
1776
|
+
routes, groups, and resources. Names resolve when the app builds (unknown
|
|
1777
|
+
names throw). Raw functions still work everywhere. See
|
|
1778
|
+
[docs/middleware.md](./docs/middleware.md).
|
|
1779
|
+
|
|
1780
|
+
[0.20.0]: https://github.com/shaferllc/keel/releases/tag/v0.20.0
|
|
1781
|
+
|
|
1782
|
+
## [0.19.0] — 2026-07-10
|
|
1783
|
+
|
|
1784
|
+
### Added
|
|
1785
|
+
|
|
1786
|
+
- **File uploads.** `request.file(name)`, `request.files(name)`, and
|
|
1787
|
+
`request.allFiles()` return web-standard `File` objects (edge-safe, no temp
|
|
1788
|
+
dir). The parsed `FormData` is cached per request, so file access and
|
|
1789
|
+
`request.all()` coexist.
|
|
1790
|
+
- **Content negotiation.** `request.accepts([...])`, `request.types()`,
|
|
1791
|
+
`request.language([...])`, `request.languages()`.
|
|
1792
|
+
- **Request meta.** `request.hasBody()`, `request.headers()`, `request.ips()`.
|
|
1793
|
+
- **Response helpers.** `response.type(mime)`, `response.append(name, value)`,
|
|
1794
|
+
`response.removeHeader(name)`, and the guards `response.abortIf(cond, …)` /
|
|
1795
|
+
`response.abortUnless(cond, …)`.
|
|
1796
|
+
|
|
1797
|
+
[0.19.0]: https://github.com/shaferllc/keel/releases/tag/v0.19.0
|
|
1798
|
+
|
|
1799
|
+
## [0.18.0] — 2026-07-10
|
|
1800
|
+
|
|
1801
|
+
### Added
|
|
1802
|
+
|
|
1803
|
+
- **Static file server.** `serveStatic(options)` serves files from a directory
|
|
1804
|
+
(default `public/`) before your routes, with `ETag` / `Last-Modified` / `304`
|
|
1805
|
+
handling, `Cache-Control` (`maxAge` / `immutable`), a dot-file policy
|
|
1806
|
+
(`ignore` / `deny` / `allow`), per-file `headers()`, and path-traversal
|
|
1807
|
+
protection. `node:fs` is imported dynamically so the core still loads on the
|
|
1808
|
+
edge. See [docs/static-files.md](./docs/static-files.md).
|
|
1809
|
+
|
|
1810
|
+
[0.18.0]: https://github.com/shaferllc/keel/releases/tag/v0.18.0
|
|
1811
|
+
|
|
1812
|
+
## [0.17.0] — 2026-07-10
|
|
1813
|
+
|
|
1814
|
+
### Added
|
|
1815
|
+
|
|
1816
|
+
- **Cache.** A memory-backed cache with TTLs and the `remember` pattern:
|
|
1817
|
+
`cache().get/put/has/forget/pull/flush`, `cache().remember(key, ttl, fn)`, and
|
|
1818
|
+
`rememberForever`. Pluggable via the `CacheStore` interface (swap in Redis/KV).
|
|
1819
|
+
See [docs/cache.md](./docs/cache.md).
|
|
1820
|
+
|
|
1821
|
+
[0.17.0]: https://github.com/shaferllc/keel/releases/tag/v0.17.0
|
|
1822
|
+
|
|
1823
|
+
## [0.16.0] — 2026-07-10
|
|
1824
|
+
|
|
1825
|
+
### Added
|
|
1826
|
+
|
|
1827
|
+
- **Events.** A tiny event emitter for decoupling — `emit(event, payload)` and
|
|
1828
|
+
`listen(event, fn)` global helpers, plus `events()` for `once` / `off` /
|
|
1829
|
+
`listenerCount` / `clear`. Listeners may be async and are awaited in order.
|
|
1830
|
+
See [docs/events.md](./docs/events.md).
|
|
1831
|
+
|
|
1832
|
+
[0.16.0]: https://github.com/shaferllc/keel/releases/tag/v0.16.0
|
|
1833
|
+
|
|
1834
|
+
## [0.15.0] — 2026-07-10
|
|
1835
|
+
|
|
1836
|
+
### Added
|
|
1837
|
+
|
|
1838
|
+
- **Sessions.** A cookie-backed session store (edge-safe, no external service):
|
|
1839
|
+
`session().get/put/has/forget/pull/increment/clear/all`, plus **flash**
|
|
1840
|
+
messages (`session().flash()` / `session().flashed()`) that survive one
|
|
1841
|
+
request. Enable with `sessionMiddleware()` in your HTTP kernel. See
|
|
1842
|
+
[docs/sessions.md](./docs/sessions.md).
|
|
1843
|
+
|
|
1844
|
+
[0.15.0]: https://github.com/shaferllc/keel/releases/tag/v0.15.0
|
|
1845
|
+
|
|
1846
|
+
## [0.14.0] — 2026-07-10
|
|
1847
|
+
|
|
1848
|
+
### Added
|
|
1849
|
+
|
|
1850
|
+
- **Request input API.** `request.all()`, `request.input(key, fallback?)`,
|
|
1851
|
+
`request.only([...])`, `request.except([...])` (merge query + parsed body),
|
|
1852
|
+
plus `request.ip()`.
|
|
1853
|
+
- **Cookies.** `request.cookie(name?)`, `response.cookie(name, value, options)`,
|
|
1854
|
+
and `response.clearCookie(name)`.
|
|
1855
|
+
- **Response helpers.** `response.send(data)` (objects → JSON, else text) and
|
|
1856
|
+
`response.abort(message, status)` (throws an `HttpException`).
|
|
1857
|
+
- See [docs/request-response.md](./docs/request-response.md).
|
|
1858
|
+
|
|
1859
|
+
[0.14.0]: https://github.com/shaferllc/keel/releases/tag/v0.14.0
|
|
1860
|
+
|
|
1861
|
+
## [0.13.0] — 2026-07-10
|
|
1862
|
+
|
|
1863
|
+
### Added
|
|
1864
|
+
|
|
1865
|
+
- **Single-action controllers.** `router.post("/publish", [PublishPost])` calls
|
|
1866
|
+
the controller's `handle` method.
|
|
1867
|
+
- **Lazy-loaded controllers.** `[() => import("../Controllers/X.js"), "index"]`
|
|
1868
|
+
— the controller is imported only when its route is first hit.
|
|
1869
|
+
- **Richer resources.** `RouteResource` gained `.as(name)`, `.params({ … })`,
|
|
1870
|
+
and `.use(actions, mw)`; `router.resource("posts.comments", C)` nests
|
|
1871
|
+
resources (`/posts/:post_id/comments/:id`).
|
|
1872
|
+
- **`make:controller --resource`** generates a controller with all seven RESTful
|
|
1873
|
+
actions.
|
|
1874
|
+
- See [docs/controllers.md](./docs/controllers.md).
|
|
1875
|
+
|
|
1876
|
+
[0.13.0]: https://github.com/shaferllc/keel/releases/tag/v0.13.0
|
|
1877
|
+
|
|
1878
|
+
## [0.12.0] — 2026-07-10
|
|
1879
|
+
|
|
1880
|
+
### Added
|
|
1881
|
+
|
|
1882
|
+
- **Inertia.js server adapter.** `inertia("Page", props)` and
|
|
1883
|
+
`router.on(path).renderInertia(...)` — full HTML on first load, JSON page
|
|
1884
|
+
object on XHR navigations, asset-version 409s, and partial reloads. Configure
|
|
1885
|
+
an `Inertia` instance (root view + version) in a provider. See
|
|
1886
|
+
[docs/inertia.md](./docs/inertia.md).
|
|
1887
|
+
- **Domain / subdomain routing.** `route.domain(pattern)` and
|
|
1888
|
+
`group(...).domain(":tenant.example.com")`, dispatched by `Host`; subdomain
|
|
1889
|
+
params via `request.subdomain(name)`.
|
|
1890
|
+
- **Route matchers & global constraints.** `router.matchers.number()/uuid()/slug()/alpha()`,
|
|
1891
|
+
a global `router.where(param, matcher)`, group `.where()`, and the `{ match }`
|
|
1892
|
+
matcher form.
|
|
1893
|
+
- **Brisk-route helpers.** `on().renderInertia()`, `on().redirectToPath()`, and
|
|
1894
|
+
`on().redirectToRoute(name, params, { qs })`.
|
|
1895
|
+
- **Current route.** `request.route` (`{ name, pattern, methods }`) and
|
|
1896
|
+
`request.routeIs(name)`.
|
|
1897
|
+
- **`.use()`** middleware alias on routes and groups.
|
|
1898
|
+
|
|
1899
|
+
### Tests
|
|
1900
|
+
|
|
1901
|
+
- Suite grown to 45 tests; ~99% line coverage maintained.
|
|
1902
|
+
|
|
1903
|
+
[0.12.0]: https://github.com/shaferllc/keel/releases/tag/v0.12.0
|
|
1904
|
+
|
|
1905
|
+
## [0.11.0] — 2026-07-10
|
|
1906
|
+
|
|
1907
|
+
### Added
|
|
1908
|
+
|
|
1909
|
+
- **First-class routing.** The router gained a fluent API:
|
|
1910
|
+
- **Named routes** + `router.url(name, params)` for URL generation.
|
|
1911
|
+
- **Route groups** — `router.group(cb).prefix().middleware().as()`.
|
|
1912
|
+
- **Resource routes** — `router.resource(name, Controller)` with
|
|
1913
|
+
`.only()`/`.except()`/`.apiOnly()`.
|
|
1914
|
+
- **Per-route middleware** — `route.middleware([...])`.
|
|
1915
|
+
- **Param constraints** — `route.where("id", /\d+/)`.
|
|
1916
|
+
- **`router.on(path).redirect(to)` / `.render(Component)`** convenience routes.
|
|
1917
|
+
- **`router.any()`** and **`router.route(methods, path, handler)`**.
|
|
1918
|
+
- `keel routes` now lists verbs and route names.
|
|
1919
|
+
|
|
1920
|
+
### Changed
|
|
1921
|
+
|
|
1922
|
+
- `RouteDefinition.method` → `methods: Method[]` (routes can match multiple
|
|
1923
|
+
verbs); route defs also carry `name`, `middleware`, and `wheres`.
|
|
1924
|
+
|
|
1925
|
+
[0.11.0]: https://github.com/shaferllc/keel/releases/tag/v0.11.0
|
|
1926
|
+
|
|
1927
|
+
## [0.10.0] — 2026-07-10
|
|
1928
|
+
|
|
1929
|
+
### Added
|
|
1930
|
+
|
|
1931
|
+
- **Request validation.** `validate(schema, data?)` parses input (the JSON body
|
|
1932
|
+
by default) and returns typed data, or throws a `ValidationException` that the
|
|
1933
|
+
kernel renders as a 422 with per-field `errors`. Schema-agnostic — works with
|
|
1934
|
+
any Zod-style `safeParse` schema, so the framework doesn't bundle a validation
|
|
1935
|
+
library. See [docs/validation.md](./docs/validation.md).
|
|
1936
|
+
|
|
1937
|
+
[0.10.0]: https://github.com/shaferllc/keel/releases/tag/v0.10.0
|
|
1938
|
+
|
|
1939
|
+
## [0.9.0] — 2026-07-10
|
|
1940
|
+
|
|
1941
|
+
### Added
|
|
1942
|
+
|
|
1943
|
+
- **Static response routes.** Pass a ready-made response as a handler, no
|
|
1944
|
+
closure: `router.get("/health", json({ status: "ok" }))`. The router clones
|
|
1945
|
+
the response per request. (Dynamic responses that read the request still use a
|
|
1946
|
+
closure, since `param()` etc. run per request.)
|
|
1947
|
+
- **`response` accessor.** Mirrors `request`: `response.json()`, `response.text()`,
|
|
1948
|
+
`response.html()`, `response.redirect()`, plus chainable `response.status(code)`
|
|
1949
|
+
and `response.header(name, value)`.
|
|
1950
|
+
|
|
1951
|
+
### Changed
|
|
1952
|
+
|
|
1953
|
+
- `json()`, `text()`, `html()`, and `redirect()` now work outside a request too
|
|
1954
|
+
(returning a plain `Response`), which is what makes static-response routes
|
|
1955
|
+
possible. Inside a handler they still build on the context.
|
|
1956
|
+
|
|
1957
|
+
[0.9.0]: https://github.com/shaferllc/keel/releases/tag/v0.9.0
|
|
1958
|
+
|
|
1959
|
+
## [0.8.0] — 2026-07-10
|
|
1960
|
+
|
|
1961
|
+
### Added
|
|
1962
|
+
|
|
1963
|
+
- **`request` accessor.** A flat view of the current request/response —
|
|
1964
|
+
`request.method`, `request.path`, `request.url`, `request.status`, plus
|
|
1965
|
+
`request.header()`, `request.param()`, `request.query()`, `request.json()`,
|
|
1966
|
+
and `request.raw`. Write `` `${request.method} ${request.path} → ${request.status}` ``
|
|
1967
|
+
in a logger without touching `c`.
|
|
1968
|
+
|
|
1969
|
+
### Changed
|
|
1970
|
+
|
|
1971
|
+
- `request` is now this accessor object rather than a function returning the raw
|
|
1972
|
+
Request; use `request.raw` for the underlying `Request`.
|
|
1973
|
+
|
|
1974
|
+
[0.8.0]: https://github.com/shaferllc/keel/releases/tag/v0.8.0
|
|
1975
|
+
|
|
1976
|
+
## [0.7.0] — 2026-07-10
|
|
1977
|
+
|
|
1978
|
+
### Added
|
|
1979
|
+
|
|
1980
|
+
- **Global container helpers.** `bind()`, `singleton()`, `instance()`,
|
|
1981
|
+
`make()`, and `bound()` operate on the active application, so you can register
|
|
1982
|
+
and resolve services from anywhere without `this.app` — e.g. `bind("clock",
|
|
1983
|
+
() => new Date())` and `make("clock")`. The `this.app.*` methods still work.
|
|
1984
|
+
|
|
1985
|
+
With this, Keel's whole surface is reachable as flat, easy-to-remember helpers:
|
|
1986
|
+
`config` · `view` · `json`/`text`/`html`/`redirect` · `param`/`query`/`body` ·
|
|
1987
|
+
`bind`/`singleton`/`instance`/`make` · `app`.
|
|
1988
|
+
|
|
1989
|
+
[0.7.0]: https://github.com/shaferllc/keel/releases/tag/v0.7.0
|
|
1990
|
+
|
|
1991
|
+
## [0.6.0] — 2026-07-10
|
|
1992
|
+
|
|
1993
|
+
### Added
|
|
1994
|
+
|
|
1995
|
+
- **Request & response helpers.** `json()`, `text()`, `html()`, `redirect()`,
|
|
1996
|
+
`param()`, `query()`, `header()`, `body()`, `request()`, and `ctx()` reach the
|
|
1997
|
+
current request without threading the context — write `json({ id: param("id") })`
|
|
1998
|
+
instead of `c.json({ id: c.req.param("id") })`. Backed by async-context storage
|
|
1999
|
+
the HTTP kernel enables per request. Taking `c` explicitly still works.
|
|
2000
|
+
|
|
2001
|
+
[0.6.0]: https://github.com/shaferllc/keel/releases/tag/v0.6.0
|
|
2002
|
+
|
|
2003
|
+
## [0.5.0] — 2026-07-10
|
|
2004
|
+
|
|
2005
|
+
### Added
|
|
2006
|
+
|
|
2007
|
+
- **Error & exception handling.** Throw `HttpException` (or `NotFoundException`,
|
|
2008
|
+
`UnauthorizedException`, `ForbiddenException`, `ValidationException`) anywhere
|
|
2009
|
+
and the HTTP kernel renders the right response — JSON or HTML by `Accept`, a
|
|
2010
|
+
readable stack-trace error page when `app.debug` is on, and hidden internals
|
|
2011
|
+
for unexpected 500s in production. Unmatched routes become a tidy 404.
|
|
2012
|
+
Customize via `kernel.onError(handler)` or by overriding `renderException`.
|
|
2013
|
+
See [docs/errors.md](./docs/errors.md).
|
|
2014
|
+
|
|
2015
|
+
[0.5.0]: https://github.com/shaferllc/keel/releases/tag/v0.5.0
|
|
2016
|
+
|
|
2017
|
+
## [0.4.0] — 2026-07-10
|
|
2018
|
+
|
|
2019
|
+
### Added
|
|
2020
|
+
|
|
2021
|
+
- **Global `view()` helper.** Render a view component in one call:
|
|
2022
|
+
`view(WelcomePage, { appName })` — props are type-checked against the
|
|
2023
|
+
component, and it returns a full HTML document. `view(HomePage)` works for
|
|
2024
|
+
components with no props. Sugar over the `View` service, matching `config()`.
|
|
2025
|
+
|
|
2026
|
+
[0.4.0]: https://github.com/shaferllc/keel/releases/tag/v0.4.0
|
|
2027
|
+
|
|
2028
|
+
## [0.3.0] — 2026-07-10
|
|
2029
|
+
|
|
2030
|
+
### Added
|
|
2031
|
+
|
|
2032
|
+
- **Global `config()` and `app()` helpers.** Read configuration from anywhere
|
|
2033
|
+
with `config("app.name")` / `config("app.port", 3000)` — no need to resolve
|
|
2034
|
+
the container by hand. `app()` returns the active application. Both resolve
|
|
2035
|
+
against the application registered automatically on construction.
|
|
2036
|
+
- **Published as `@shaferllc/keel`.** The framework is now a proper npm package
|
|
2037
|
+
with a real build (compiled JS + `.d.ts` in `dist/`). Apps install it with
|
|
2038
|
+
`npm install @shaferllc/keel` and import from `@shaferllc/keel/core`, so they
|
|
2039
|
+
receive core updates through `npm update`.
|
|
2040
|
+
|
|
2041
|
+
### Changed
|
|
2042
|
+
|
|
2043
|
+
- Documentation and copy no longer describe Keel by comparison to other
|
|
2044
|
+
frameworks — it stands on its own.
|
|
2045
|
+
|
|
2046
|
+
[0.3.0]: https://github.com/shaferllc/keel/releases/tag/v0.3.0
|
|
2047
|
+
|
|
2048
|
+
## [0.2.0] — 2026-07-10
|
|
2049
|
+
|
|
2050
|
+
Views, and a core that runs on the edge.
|
|
2051
|
+
|
|
2052
|
+
### Added
|
|
2053
|
+
|
|
2054
|
+
- **View layer** — a `View` service that renders [Hono JSX](https://hono.dev)
|
|
2055
|
+
components to HTML. Views live in `resources/views/`; layouts are just
|
|
2056
|
+
components. Platform-neutral, so the same views run on Node and Cloudflare
|
|
2057
|
+
Workers. See [docs/views.md](./docs/views.md).
|
|
2058
|
+
- **`keel/core` package export** — the framework core is now installable by
|
|
2059
|
+
other apps (`import { Application } from "keel/core"`). Only `src/core` ships
|
|
2060
|
+
in the published package.
|
|
2061
|
+
|
|
2062
|
+
### Changed
|
|
2063
|
+
|
|
2064
|
+
- **Workers-safe core** — `Application` no longer statically imports Node
|
|
2065
|
+
built-ins or dotenv; they're loaded dynamically only when filesystem config
|
|
2066
|
+
discovery runs. `boot(providers, { discoverConfig: false, config })` lets you
|
|
2067
|
+
configure inline on runtimes without a filesystem (e.g. Cloudflare Workers).
|
|
2068
|
+
|
|
2069
|
+
[0.2.0]: https://github.com/shaferllc/keel/releases/tag/v0.2.0
|
|
2070
|
+
|
|
2071
|
+
## [0.1.0] — 2026-07-10
|
|
2072
|
+
|
|
2073
|
+
The first release: the **MVP core**. Enough of a framework to build and serve a
|
|
2074
|
+
real application.
|
|
2075
|
+
|
|
2076
|
+
### Added
|
|
2077
|
+
|
|
2078
|
+
- **Service container** — `bind` / `singleton` / `instance` / `make`, with
|
|
2079
|
+
string, symbol, and class tokens and auto-construction of unbound classes.
|
|
2080
|
+
- **Application kernel** — loads `.env`, auto-loads `config/*.ts`, and runs the
|
|
2081
|
+
service-provider `register()` → `boot()` lifecycle.
|
|
2082
|
+
- **Configuration** — dot-notation `Config` repository plus a type-coercing
|
|
2083
|
+
`env()` helper.
|
|
2084
|
+
- **Service providers** — `ServiceProvider` base class and a `bootstrap`
|
|
2085
|
+
provider list.
|
|
2086
|
+
- **Routing** — a `Router` facade over Hono; handlers may be closures or
|
|
2087
|
+
`[Controller, method]` tuples resolved from the container.
|
|
2088
|
+
- **HTTP kernel** — global middleware stack that compiles routes onto Hono,
|
|
2089
|
+
served by `@hono/node-server`. Ships with a request-logging middleware.
|
|
2090
|
+
- **Console (`keel`)** — `serve`, `routes`, and `make:controller`,
|
|
2091
|
+
`make:provider`, `make:middleware` generators with an overwrite guard.
|
|
2092
|
+
- **Documentation** — getting started, container, providers, configuration,
|
|
2093
|
+
routing, middleware, console, and architecture guides.
|
|
2094
|
+
|
|
2095
|
+
[0.1.0]: https://github.com/shaferllc/keel/releases/tag/v0.1.0
|