@shaferllc/keel 0.83.0 → 0.83.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.
Files changed (51) hide show
  1. package/README.md +2 -2
  2. package/dist/billing/billable.d.ts +2 -3
  3. package/dist/billing/billable.js +2 -3
  4. package/dist/billing/billing.config.stub +1 -1
  5. package/dist/billing/builder.d.ts +2 -2
  6. package/dist/billing/builder.js +2 -2
  7. package/dist/billing/provider.d.ts +1 -1
  8. package/dist/billing/provider.js +2 -2
  9. package/dist/billing/subscription-item.d.ts +0 -1
  10. package/dist/billing/subscription-item.js +0 -1
  11. package/dist/core/decorators.d.ts +5 -7
  12. package/dist/core/decorators.js +5 -7
  13. package/dist/core/exceptions.d.ts +2 -3
  14. package/dist/core/exceptions.js +2 -3
  15. package/dist/core/model-query.d.ts +1 -1
  16. package/dist/core/model-query.js +1 -1
  17. package/dist/core/request-logger.d.ts +2 -2
  18. package/dist/core/request-logger.js +2 -2
  19. package/dist/core/request.js +1 -1
  20. package/dist/core/testing.d.ts +3 -3
  21. package/dist/core/testing.js +3 -3
  22. package/docs/ai-manifest.json +14 -14
  23. package/docs/billing.md +45 -6
  24. package/docs/changelog.md +124 -21
  25. package/docs/cors.md +28 -0
  26. package/docs/decorators.md +2 -6
  27. package/docs/errors.md +1 -1
  28. package/docs/examples/billing.ts +128 -0
  29. package/docs/examples/cors.ts +29 -0
  30. package/docs/examples/hono.ts +29 -0
  31. package/docs/examples/openapi.ts +35 -0
  32. package/docs/examples/orm.ts +30 -0
  33. package/docs/examples/packages.ts +26 -0
  34. package/docs/examples/query-builder.ts +118 -0
  35. package/docs/examples/security.ts +31 -0
  36. package/docs/examples/social-auth.ts +76 -0
  37. package/docs/examples/starter-kits.ts +14 -0
  38. package/docs/examples/watch.ts +10 -0
  39. package/docs/hooks.md +1 -3
  40. package/docs/logger.md +2 -2
  41. package/docs/orm.md +39 -0
  42. package/docs/packages.md +3 -3
  43. package/docs/providers.md +6 -6
  44. package/docs/request-response.md +2 -2
  45. package/docs/starter-kits.md +14 -0
  46. package/docs/testing.md +1 -2
  47. package/docs/validation.md +1 -2
  48. package/docs/watch.md +1 -1
  49. package/llms-full.txt +145 -34
  50. package/llms.txt +13 -2
  51. package/package.json +1 -1
package/docs/changelog.md CHANGED
@@ -4,6 +4,110 @@ All notable changes to Keel are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to
5
5
  adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.83.2] — 2026-07-12
8
+
9
+ ### Added
10
+
11
+ - **More runnable doc examples.** Billing, ORM, query builder, CORS, security,
12
+ social auth, OpenAPI, Watch, packages, Hono, and starter kits now have
13
+ typechecked harnesses under `docs/examples/` — the same ones
14
+ `npm run typecheck:docs` compiles against the published surface.
15
+ - **Worked examples in the guides** — a complete billing subscribe flow (with
16
+ `FakeGateway`), an ORM CRUD+eager-load walkthrough, a production CORS recipe,
17
+ and a clearer starter-kit picker.
18
+
19
+ ### Changed
20
+
21
+ - Watch no longer describes itself by another product's name.
22
+
23
+ [0.83.2]: https://github.com/shaferllc/keel/releases/tag/v0.83.2
24
+
25
+ ## [0.83.0] — 2026-07-12
26
+
27
+ ### Added
28
+
29
+ - **Starter kits, and a generator that can't fall behind.** Four curated
30
+ applications — `minimal`, `api`, `app` (views, sessions, register/login, password
31
+ reset, two-factor), and `saas` (teams, roles, invitations, billing,
32
+ multi-tenancy):
33
+
34
+ ```bash
35
+ npm create keeljs@latest my-app -- --preset saas
36
+ ```
37
+
38
+ The templates ship **inside this package**, so the version a kit is generated from
39
+ is, by construction, the version it was written for. The old standalone starter
40
+ drifted to five releases behind and nothing noticed; CI now generates all four kits
41
+ on every push and typechecks, migrates, boots, serves a request, bundles the
42
+ Worker, and runs their tests.
43
+
44
+ - **Teams (`@shaferllc/keel/teams`)** — multi-tenancy, membership, roles, and
45
+ invitations. `TenantModel` makes isolation the default rather than a habit: reads
46
+ are constrained by an inherited global scope (so even `find(id)` returns `null` for
47
+ another team's row — not a filter you can forget), and writes are stamped with the
48
+ current team, so a row can't be born ownerless.
49
+
50
+ Outside a team context a tenant query **throws**. A job, console command, or
51
+ webhook has no request and therefore no team; returning everything is how one
52
+ customer's data reaches another, and `team_id = NULL` means jobs quietly do
53
+ nothing. So work says which team it is for — `runForTeam(team, fn)` — or says out
54
+ loud that it spans all of them — `withoutTenant(fn)`. Both are greppable at audit
55
+ time.
56
+
57
+ - **Accounts (`@shaferllc/keel/accounts`)** — password reset, email verification, and
58
+ two-factor, mounted with one provider. A correct password on a 2FA account yields a
59
+ short-lived, single-purpose **challenge, not a session**, so there is no
60
+ half-authenticated state for a route to forget to check. TOTP is RFC 6238, verified
61
+ against the RFC's published vectors, WebCrypto-only, and therefore edge-safe. No
62
+ tokens table: reset links carry their own purpose and expiry and are bound to a
63
+ fingerprint of the current password hash, so spending one kills it.
64
+
65
+ - **D1 over HTTP (`@shaferllc/keel/db/d1-http`).** The D1 binding only exists inside a
66
+ Worker, so `keel migrate` had nowhere to point and you could not create your
67
+ tables. The same `Connection` over D1's HTTP API closes that: migrations run from a
68
+ laptop and from CI. It treats an error in the response *body* as an error even when
69
+ the status is 200 — which is how Cloudflare often reports them, and trusting the
70
+ status would let a failed migration look like it succeeded.
71
+
72
+ - **`Model.withoutGlobalScope(...)` / `withoutGlobalScopes()`** — escaping a scope
73
+ should be typed out and greppable, never something you arrive at by forgetting a
74
+ `where`.
75
+
76
+ ### Fixed
77
+
78
+ - **Global scopes and model hooks now inherit.** Both were keyed by the exact class,
79
+ so a scope or a `creating` hook declared on a *base* class was silently ignored by
80
+ every subclass. The models guide advertises global scopes as "the base for
81
+ multi-tenancy", and that was precisely the case that didn't work: the scope did
82
+ nothing, the query returned every tenant's rows, and nothing complained. It failed
83
+ **open**. Both now walk the prototype chain, and a scope is passed the model it is
84
+ scoping so a base class can read each subclass's own configuration.
85
+
86
+ - **The official `@libsql/client` no longer needs a cast.** Under
87
+ `strictFunctionTypes` the narrower parameter types made the real `Client`
88
+ unassignable to `LibSqlLike`, so wiring libSQL the obvious way required
89
+ `client as unknown as LibSqlLike` — a cast even Keel's own tests carried.
90
+
91
+ - **`createTeam()` could not give two people with the same name a team.** It slugged
92
+ the name into a `UNIQUE` column, so the second "Ada" to sign up got a 500. Looking
93
+ for a free slug first is check-then-act and loses the race anyway; the unique index
94
+ is the only real arbiter, so it now arbitrates and the insert retries.
95
+
96
+ ## [0.82.0] — 2026-07-12
97
+
98
+ ### Added
99
+
100
+ - **Query builder: the methods people actually reach for.** `join`/`leftJoin`,
101
+ `groupBy`/`having`/`distinct`, `whereColumn`/`whereRaw`/`orderByRaw`, `when`,
102
+ `increment`/`decrement`, `upsert`/`insertOrIgnore`, and `chunk`, with docs.
103
+
104
+ ## [0.81.2] — 2026-07-12
105
+
106
+ ### Documentation
107
+
108
+ - **The changelog is published to the docs site** (`docs/changelog.md`), so releases
109
+ are readable at keeljs.com rather than only in the repository.
110
+
7
111
  ## [0.81.1] — 2026-07-12
8
112
 
9
113
  ### Documentation
@@ -25,8 +129,8 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
25
129
 
26
130
  ### Added
27
131
 
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
132
+ - **ORM: the features people reach for.** The active-record `Model` grows the
133
+ surface a real ORM needs, all backward-compatible and still on the
30
134
  driver-agnostic query builder (no JOINs, edge-safe):
31
135
 
32
136
  - **Lifecycle events & observers** — `creating`/`created`, `updating`/
@@ -91,8 +195,8 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
91
195
 
92
196
  ### Added
93
197
 
94
- - **Billing** — a new `@shaferllc/keel/billing` package: a Cashier-style
95
- subscription layer with **one gateway-neutral API over Stripe and Paddle**
198
+ - **Billing** — a new `@shaferllc/keel/billing` package: a subscription
199
+ layer with **one gateway-neutral API over Stripe and Paddle**
96
200
  (switching gateways is a config change). `class User extends Billable(Model)`
97
201
  gives a gateway customer, subscriptions (create/swap/quantity/trials/cancel/
98
202
  resume + status checks), single charges + refunds, invoices, hosted checkout,
@@ -286,7 +390,7 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
286
390
 
287
391
  ### Changed
288
392
 
289
- - **Removed the Remult references** from the API-resources guide and source
393
+ - **Removed comparisons to other frameworks** from the API-resources guide and source
290
394
  comments. Keel isn't that, and the docs shouldn't read as a comparison to another
291
395
  framework. The surrounding sentences are rewritten so they still say what the
292
396
  feature does rather than leaving a hole. No behavior change.
@@ -960,7 +1064,7 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
960
1064
  connection. They honor `X-Forwarded-Proto` / `X-Forwarded-Host` over the raw
961
1065
  URL, so an app behind a TLS-terminating proxy or load balancer sees the
962
1066
  client's real scheme and host — use `origin` to build absolute links and
963
- `secure` to gate insecure requests. (Koa-inspired.)
1067
+ `secure` to gate insecure requests.
964
1068
  - **`response.back(fallback?)` and `redirect("back")`.** Redirect to the
965
1069
  request's `Referer`, falling back to `fallback` (default `"/"`) when it's
966
1070
  absent — the "return where you came from" shortcut for post-form flows.
@@ -1189,7 +1293,7 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1189
1293
  - **Route config / metadata.** Attach arbitrary data to a route or group with
1190
1294
  `.config({ … })` and read it in the handler or route middleware via
1191
1295
  `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,
1296
+ layout choice. Group config merges into every route,
1193
1297
  with a route's own keys winning. The matched-route context is now set *before*
1194
1298
  a route's middleware, so route/group middleware can branch on `request.route`.
1195
1299
  See [docs/routing.md](./docs/routing.md#route-config).
@@ -1236,7 +1340,7 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1236
1340
  - **Response header helpers.** The `response` accessor gains `headers({...})` (set
1237
1341
  several at once), `getHeader(name)`, and `hasHeader(name)` — so middleware can
1238
1342
  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.
1343
+ a default `cache-control`).
1240
1344
  See [docs/request-response.md](./docs/request-response.md).
1241
1345
  - **Design principles** documented in
1242
1346
  [docs/architecture.md](./docs/architecture.md#design-principles) — edge-safe /
@@ -1253,9 +1357,9 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1253
1357
  options at registration: `app.register(RateLimitProvider, { max: 100 })`, typed
1254
1358
  via `ServiceProvider<{ max: number }>` and read as `this.options`. The same
1255
1359
  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
1360
+ is now genuinely reusable. Backward compatible options default to `{}`.
1361
+ (Keel providers stay un-encapsulated by design; per-request scoping is
1362
+ [middleware](./docs/middleware.md).) See
1259
1363
  [docs/providers.md](./docs/providers.md#providers-are-keels-plugin-system).
1260
1364
 
1261
1365
  [0.50.0]: https://github.com/shaferllc/keel/releases/tag/v0.50.0
@@ -1350,8 +1454,8 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1350
1454
  a bad request with a `422` `ValidationException` (errors from every part
1351
1455
  aggregated, keyed `body.field` / `query.field` / `params.field`) so the handler
1352
1456
  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
1457
+ Built on the same schema-agnostic `validate()` engine (bring your own
1458
+ Zod-style schema). See
1355
1459
  [docs/validation.md](./docs/validation.md#declarative-validation-before-the-handler).
1356
1460
 
1357
1461
  [0.44.0]: https://github.com/shaferllc/keel/releases/tag/v0.44.0
@@ -1362,7 +1466,7 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1362
1466
 
1363
1467
  - **Per-request logging.** `requestLogger()` middleware binds a child logger with
1364
1468
  a generated `reqId` to each request, so every log line within a request
1365
- correlates (Fastify's `request.log`). It logs request start/completion
1469
+ correlates. It logs request start/completion
1366
1470
  (method, path, status, ms) by default; options for `genReqId`, reusing an
1367
1471
  incoming `idHeader` (distributed tracing), and disabling the auto lines.
1368
1472
  `requestLog()` reaches the current request's logger anywhere (falls back to the
@@ -1400,8 +1504,8 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1400
1504
  through the default path (with `code` in the JSON body) and passes
1401
1505
  `instanceof HttpException`. The built-in exceptions now carry stable codes too
1402
1506
  (`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
1507
+ surfaces without any work. Also documented: serving over **HTTP/2** needs no
1508
+ framework code — it's a transport
1405
1509
  concern handled by the edge platform, a reverse proxy, or a `@hono/node-server`
1406
1510
  `node:http2` option. See [docs/errors.md](./docs/errors.md#coded-errors-with-createerror)
1407
1511
  and [docs/hono.md](./docs/hono.md#serving-over-http2).
@@ -1435,8 +1539,8 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1435
1539
  - **Raw request-body accessors.** `request.text()`, `request.arrayBuffer()`, and
1436
1540
  `request.blob()` read the body for content types `json()` / `all()` don't
1437
1541
  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
1542
+ parse yourself. Keel keeps body parsing explicit (no content-type parser
1543
+ registry): you call the accessor you want. See
1440
1544
  [docs/request-response.md](./docs/request-response.md#other-content-types).
1441
1545
 
1442
1546
  [0.40.1]: https://github.com/shaferllc/keel/releases/tag/v0.40.1
@@ -1450,9 +1554,8 @@ adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1450
1554
  life of the request. `decorateRequest(name, resolver)` registers a resolver
1451
1555
  (sync or async), `decorated(name)` reads it (computed once, then cached),
1452
1556
  `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.
1557
+ `hasRequestDecorator(name)` checks. The per-request memo is keyed off the
1558
+ context via a WeakMap, so nothing leaks between requests.
1456
1559
  (Decorating the *app* is already the container's job.) See
1457
1560
  [docs/decorators.md](./docs/decorators.md).
1458
1561
 
package/docs/cors.md CHANGED
@@ -21,6 +21,34 @@ router.group(() => { /* … */ }).use(cors({ origin: ["https://app.example.com"]
21
21
  With no options, `cors()` reflects the caller's origin — convenient in
22
22
  development. **Lock it down in production** with an explicit allowlist.
23
23
 
24
+ ## A production API group
25
+
26
+ Typical setup for a JSON API served from `api.example.com` and called from a
27
+ SPA on `app.example.com`:
28
+
29
+ ```ts
30
+ // app/Http/Kernel.ts
31
+ import { cors } from "@shaferllc/keel/core";
32
+
33
+ this.use(
34
+ cors({
35
+ origin: ["https://app.example.com"],
36
+ credentials: true, // cookies / Authorization
37
+ exposeHeaders: ["X-Request-Id"],
38
+ }),
39
+ );
40
+ ```
41
+
42
+ During local development, allow any localhost port with a predicate:
43
+
44
+ ```ts
45
+ cors({
46
+ origin: (origin) =>
47
+ origin.startsWith("http://localhost:") || origin === "https://app.example.com",
48
+ credentials: true,
49
+ });
50
+ ```
51
+
24
52
  ## Options
25
53
 
26
54
  ```ts
@@ -2,10 +2,9 @@
2
2
 
3
3
  Attach named, computed values to the current request — `request.user`,
4
4
  `request.tenant`, `request.locale` — resolved **lazily** and **memoized for the
5
- life of the request**. Inspired by [Fastify's decorators](https://fastify.dev/docs/latest/Reference/Decorators/),
6
- but without the footguns: you register a resolver once, and Keel computes it on
5
+ life of the request**. You register a resolver once, and Keel computes it on
7
6
  first access and caches it per request. No null-placeholder declaration, no
8
- `onRequest` hook to remember, no shared-state leak between requests.
7
+ shared-state leak between requests.
9
8
 
10
9
  > Decorating the **application** is already the [service container's](./container.md)
11
10
  > job — `bind` / `singleton` / `instance` / `make`, with `bound()` as
@@ -53,9 +52,6 @@ setRequestValue("user", theAuthenticatedUser);
53
52
  const user = await decorated("user"); // returns the value set above, no re-lookup
54
53
  ```
55
54
 
56
- This is the clean version of Fastify's "declare a placeholder, set it in an
57
- `onRequest` hook" pattern.
58
-
59
55
  ## Why lazy + memoized
60
56
 
61
57
  Resolving the current user (or tenant, or locale, or a feature-flag set) is the
package/docs/errors.md CHANGED
@@ -185,7 +185,7 @@ subclasses don't define either — they render through the default path.
185
185
 
186
186
  When all you want is a coded error class — a stable `code`, a message, a status —
187
187
  skip the boilerplate and mint one with `createError`. It's the ergonomic shortcut
188
- for the common case, inspired by Fastify's `@fastify/error`:
188
+ for the common case:
189
189
 
190
190
  ```ts
191
191
  import { createError } from "@shaferllc/keel/core";
@@ -0,0 +1,128 @@
1
+ // Type-check harness for docs/billing.md. Compile-only — never executed.
2
+ import { Model, listen } from "@shaferllc/keel/core";
3
+ import {
4
+ Billable,
5
+ BillingServiceProvider,
6
+ BillingManager,
7
+ FakeGateway,
8
+ setBilling,
9
+ resolveBillableUsing,
10
+ type CheckoutSession,
11
+ type GatewayCharge,
12
+ } from "@shaferllc/keel/billing";
13
+
14
+ export class User extends Billable(Model) {
15
+ static override table = "users";
16
+ declare id: number;
17
+ declare email: string;
18
+ declare name: string;
19
+ }
20
+
21
+ declare const user: User;
22
+ declare const paymentMethodId: string;
23
+ declare const successUrl: string;
24
+ declare const cancelUrl: string;
25
+
26
+ export const providers = [BillingServiceProvider];
27
+
28
+ /* -------------------------------- customers ------------------------------- */
29
+
30
+ export async function customers() {
31
+ await user.createAsCustomer();
32
+ user.hasBillingId();
33
+ await user.getCustomerId();
34
+ }
35
+
36
+ /* ------------------------------ subscriptions ----------------------------- */
37
+
38
+ export async function subscribe() {
39
+ await user
40
+ .newSubscription("default", "price_pro")
41
+ .trialDays(14)
42
+ .quantity(3)
43
+ .create(paymentMethodId);
44
+
45
+ const session: CheckoutSession = await user
46
+ .newSubscription("default", "price_pro")
47
+ .checkout({ successUrl, cancelUrl });
48
+ return session;
49
+ }
50
+
51
+ export async function status() {
52
+ await user.subscribed();
53
+ await user.subscribedToPrice("price_pro");
54
+ await user.onTrial();
55
+ const sub = await user.subscription();
56
+ if (!sub) return;
57
+
58
+ sub.active();
59
+ sub.onTrial();
60
+ sub.recurring();
61
+ sub.canceled();
62
+ sub.onGracePeriod();
63
+ sub.ended();
64
+ sub.paused();
65
+ sub.valid();
66
+ sub.hasIncompletePayment();
67
+
68
+ await sub.swap("price_enterprise");
69
+ await sub.updateQuantity(10);
70
+ await sub.incrementQuantity(2);
71
+ await sub.decrementQuantity();
72
+ await sub.cancel();
73
+ await sub.resume();
74
+ await sub.cancelNow();
75
+ await sub.endTrial();
76
+ await sub.extendTrial(new Date("2026-01-01"));
77
+ }
78
+
79
+ /* -------------------------------- charges --------------------------------- */
80
+
81
+ export async function charges() {
82
+ const charge: GatewayCharge = await user.charge(2000, {
83
+ paymentMethod: "pm_1",
84
+ description: "Credits",
85
+ });
86
+ await user.refund(charge.id);
87
+ await user.refund(charge.id, 500);
88
+ await user.checkout("price_onetime", { successUrl, cancelUrl });
89
+ await user.createSetupIntent();
90
+ await user.paymentMethods();
91
+ await user.invoices();
92
+ }
93
+
94
+ /* -------------------------------- webhooks -------------------------------- */
95
+
96
+ export function webhooks() {
97
+ listen("billing.subscription.updated", (e) => {
98
+ void e.gateway;
99
+ void e.subscriptionId;
100
+ });
101
+ listen("billing.webhook.received", (e) => {
102
+ void e.gateway;
103
+ void e.type;
104
+ });
105
+ resolveBillableUsing(async (customerId, _gateway) => {
106
+ return { id: user.id, type: "User" };
107
+ });
108
+ }
109
+
110
+ /* --------------------------------- testing -------------------------------- */
111
+
112
+ export function fakeGateway() {
113
+ const fake = new FakeGateway();
114
+ const manager = new BillingManager({
115
+ default: "fake",
116
+ currency: "usd",
117
+ billableModel: "User",
118
+ webhook: { path: "billing/webhook" },
119
+ gateways: {
120
+ stripe: { key: "", webhookSecret: "" },
121
+ paddle: { key: "", webhookSecret: "" },
122
+ fake: {},
123
+ },
124
+ });
125
+ manager.register("fake", () => fake);
126
+ setBilling(manager);
127
+ return fake;
128
+ }
@@ -0,0 +1,29 @@
1
+ // Type-check harness for docs/cors.md. Compile-only — never executed.
2
+ import type { MiddlewareHandler } from "hono";
3
+ import { cors, type CorsOptions } from "@shaferllc/keel/core";
4
+
5
+ export function kernel(): MiddlewareHandler {
6
+ return cors();
7
+ }
8
+
9
+ export function apiGroup(): MiddlewareHandler {
10
+ return cors({ origin: ["https://app.example.com"] });
11
+ }
12
+
13
+ export function production(): MiddlewareHandler {
14
+ const options: CorsOptions = {
15
+ origin: ["https://app.example.com"],
16
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
17
+ headers: true,
18
+ exposeHeaders: ["X-Request-Id"],
19
+ credentials: true,
20
+ maxAge: 86400,
21
+ };
22
+ return cors(options);
23
+ }
24
+
25
+ export function dynamicOrigin(): MiddlewareHandler {
26
+ return cors({
27
+ origin: (origin) => origin.endsWith(".example.com") || origin.startsWith("http://localhost"),
28
+ });
29
+ }
@@ -0,0 +1,29 @@
1
+ // Type-check harness for docs/hono.md. Compile-only — never executed.
2
+ import type { Ctx } from "@shaferllc/keel/core";
3
+ import { Router, json } from "@shaferllc/keel/core";
4
+
5
+ export function withContext(router: Router) {
6
+ router.get("/users/:id", async (c: Ctx) => {
7
+ const id = c.req.param("id");
8
+ const q = c.req.query("q");
9
+ const auth = c.req.header("authorization");
10
+ void q;
11
+ void auth;
12
+ return c.json({ id });
13
+ });
14
+
15
+ router.get("/ping", (c) => c.text("pong"));
16
+ router.get("/go", (c) => c.redirect("/"));
17
+ }
18
+
19
+ export function ambientHelpers() {
20
+ return json({ ok: true });
21
+ }
22
+
23
+ export function keelContext(c: Ctx) {
24
+ return {
25
+ app: c.get("app"),
26
+ route: c.get("route"),
27
+ subdomains: c.get("subdomains"),
28
+ };
29
+ }
@@ -0,0 +1,35 @@
1
+ // Type-check harness for docs/openapi.md. Compile-only — never executed.
2
+ import { z } from "zod";
3
+ import { Application, Router, validateRequest, type Ctx } from "@shaferllc/keel/core";
4
+ import { OpenApiServiceProvider, apiDoc, OpenApi } from "@shaferllc/keel/openapi";
5
+
6
+ const NewUser = z.object({ email: z.string().email(), age: z.number().min(18) });
7
+ const UserShape = z.object({ id: z.number(), email: z.string().email() });
8
+
9
+ class Users {
10
+ store(_c: Ctx) {
11
+ return { id: 1, email: "a@b.com" };
12
+ }
13
+ }
14
+
15
+ export function install() {
16
+ const app = new Application();
17
+ app.register(OpenApiServiceProvider);
18
+ OpenApi.auth((c) => c.req.header("x-docs-key") === "secret");
19
+ return app;
20
+ }
21
+
22
+ export function document(router: Router) {
23
+ router
24
+ .post("/users", [Users, "store"])
25
+ .name("users.store")
26
+ .config(
27
+ apiDoc({
28
+ summary: "Create a user",
29
+ tags: ["users"],
30
+ request: { body: NewUser },
31
+ responses: { 201: { description: "The created user", schema: UserShape } },
32
+ }),
33
+ )
34
+ .middleware([validateRequest({ body: NewUser })]);
35
+ }
@@ -0,0 +1,30 @@
1
+ // Type-check harness for docs/orm.md. Compile-only — never executed.
2
+ import { Model } from "@shaferllc/keel/core";
3
+
4
+ class Post extends Model {
5
+ static override table = "posts";
6
+ declare id: number;
7
+ declare title: string;
8
+ declare user_id: number;
9
+ }
10
+
11
+ export class User extends Model {
12
+ static override table = "users";
13
+ declare id: number;
14
+ declare email: string;
15
+
16
+ posts() {
17
+ return this.hasMany(Post);
18
+ }
19
+ }
20
+
21
+ export async function map() {
22
+ const user = await User.find(1);
23
+ if (!user) return;
24
+ await user.posts();
25
+ await User.with("posts").where("email", user.email).first();
26
+ await User.create({ email: "ada@example.com" });
27
+ user.email = "grace@example.com";
28
+ await user.save();
29
+ await user.delete();
30
+ }
@@ -0,0 +1,26 @@
1
+ // Type-check harness for docs/packages.md. Compile-only — never executed.
2
+ import { PackageProvider, type Router, type Migration, type PackageCommand } from "@shaferllc/keel/core";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join } from "node:path";
5
+
6
+ const here = dirname(fileURLToPath(import.meta.url));
7
+
8
+ declare const createInvoicesTable: Migration;
9
+ declare const syncInvoicesCommand: PackageCommand;
10
+ declare function registerBillingRoutes(r: Router): void;
11
+
12
+ export class BillingServiceProvider extends PackageProvider {
13
+ readonly name = "billing";
14
+
15
+ register(): void {
16
+ this.mergeConfig("billing", { enabled: true, path: "billing" });
17
+ this.migrations([createInvoicesTable]);
18
+ this.publishes({ [join(here, "config.stub")]: "config/billing.ts" }, "billing-config");
19
+ this.commands([syncInvoicesCommand]);
20
+ }
21
+
22
+ boot(): void {
23
+ this.assets("billing/assets", join(here, "ui/dist"), { maxAge: 3600 });
24
+ this.routes((r: Router) => registerBillingRoutes(r), { prefix: "billing", as: "billing" });
25
+ }
26
+ }