@shaferllc/keel 0.59.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/AGENTS.md +167 -0
  2. package/README.md +30 -1
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/core/application.d.ts +5 -5
  5. package/dist/core/application.js +14 -2
  6. package/dist/core/auth.d.ts +47 -0
  7. package/dist/core/auth.js +77 -0
  8. package/dist/core/authorization.d.ts +9 -1
  9. package/dist/core/authorization.js +22 -2
  10. package/dist/core/cache.d.ts +82 -5
  11. package/dist/core/cache.js +181 -23
  12. package/dist/core/cli/stubs.d.ts +12 -0
  13. package/dist/core/cli/stubs.js +120 -0
  14. package/dist/core/container.d.ts +20 -0
  15. package/dist/core/container.js +52 -0
  16. package/dist/core/cors.d.ts +29 -0
  17. package/dist/core/cors.js +72 -0
  18. package/dist/core/crypto.d.ts +40 -4
  19. package/dist/core/crypto.js +66 -6
  20. package/dist/core/csrf.d.ts +25 -0
  21. package/dist/core/csrf.js +78 -0
  22. package/dist/core/database.d.ts +49 -4
  23. package/dist/core/database.js +89 -21
  24. package/dist/core/events.d.ts +129 -5
  25. package/dist/core/events.js +165 -7
  26. package/dist/core/health.d.ts +141 -0
  27. package/dist/core/health.js +226 -0
  28. package/dist/core/helpers.d.ts +15 -3
  29. package/dist/core/helpers.js +23 -3
  30. package/dist/core/index.d.ts +33 -18
  31. package/dist/core/index.js +16 -8
  32. package/dist/core/lock.d.ts +139 -0
  33. package/dist/core/lock.js +215 -0
  34. package/dist/core/logger.d.ts +82 -4
  35. package/dist/core/logger.js +141 -23
  36. package/dist/core/mail.d.ts +128 -7
  37. package/dist/core/mail.js +264 -16
  38. package/dist/core/model.d.ts +2 -0
  39. package/dist/core/model.js +16 -14
  40. package/dist/core/provider.d.ts +7 -0
  41. package/dist/core/provider.js +7 -0
  42. package/dist/core/queue.d.ts +134 -9
  43. package/dist/core/queue.js +304 -14
  44. package/dist/core/rate-limit.js +3 -0
  45. package/dist/core/relations.js +13 -13
  46. package/dist/core/request.d.ts +26 -0
  47. package/dist/core/request.js +77 -0
  48. package/dist/core/shield.d.ts +39 -0
  49. package/dist/core/shield.js +60 -0
  50. package/dist/core/social.d.ts +173 -0
  51. package/dist/core/social.js +337 -0
  52. package/dist/core/storage.d.ts +159 -6
  53. package/dist/core/storage.js +287 -7
  54. package/dist/core/tokens.d.ts +74 -0
  55. package/dist/core/tokens.js +155 -0
  56. package/dist/db/d1.d.ts +32 -0
  57. package/dist/db/d1.js +26 -0
  58. package/dist/db/libsql.d.ts +29 -0
  59. package/dist/db/libsql.js +32 -0
  60. package/dist/db/pg.d.ts +29 -0
  61. package/dist/db/pg.js +33 -0
  62. package/dist/mcp/server.d.ts +19 -0
  63. package/dist/mcp/server.js +355 -0
  64. package/docs/ai-manifest.json +2472 -0
  65. package/docs/ai.md +128 -0
  66. package/docs/architecture.md +331 -0
  67. package/docs/authentication.md +453 -0
  68. package/docs/authorization.md +167 -0
  69. package/docs/broadcasting.md +137 -0
  70. package/docs/broker.md +500 -0
  71. package/docs/cache.md +558 -0
  72. package/docs/configuration.md +311 -0
  73. package/docs/console.md +356 -0
  74. package/docs/container.md +467 -0
  75. package/docs/controllers.md +265 -0
  76. package/docs/cors.md +51 -0
  77. package/docs/database.md +530 -0
  78. package/docs/debugging.md +129 -0
  79. package/docs/decorators.md +127 -0
  80. package/docs/errors.md +395 -0
  81. package/docs/events.md +496 -0
  82. package/docs/examples/architecture-app.ts +27 -0
  83. package/docs/examples/authentication.ts +61 -0
  84. package/docs/examples/authorization.ts +79 -0
  85. package/docs/examples/broadcasting.ts +60 -0
  86. package/docs/examples/broker-cache-validate.ts +34 -0
  87. package/docs/examples/broker-fault-tolerance.ts +29 -0
  88. package/docs/examples/broker-middleware.ts +27 -0
  89. package/docs/examples/broker.ts +203 -0
  90. package/docs/examples/cache.ts +222 -0
  91. package/docs/examples/configuration.ts +81 -0
  92. package/docs/examples/container.ts +134 -0
  93. package/docs/examples/controllers.ts +86 -0
  94. package/docs/examples/database.ts +118 -0
  95. package/docs/examples/debugging.ts +41 -0
  96. package/docs/examples/decorators.ts +40 -0
  97. package/docs/examples/errors.ts +121 -0
  98. package/docs/examples/events.ts +204 -0
  99. package/docs/examples/factories.ts +84 -0
  100. package/docs/examples/hashing.ts +71 -0
  101. package/docs/examples/health.ts +94 -0
  102. package/docs/examples/helpers.ts +171 -0
  103. package/docs/examples/hooks.ts +54 -0
  104. package/docs/examples/inertia.ts +81 -0
  105. package/docs/examples/locks.ts +120 -0
  106. package/docs/examples/logger.ts +92 -0
  107. package/docs/examples/mail.ts +160 -0
  108. package/docs/examples/middleware.ts +119 -0
  109. package/docs/examples/migrations.ts +126 -0
  110. package/docs/examples/models.ts +239 -0
  111. package/docs/examples/notification.ts +124 -0
  112. package/docs/examples/providers.ts +123 -0
  113. package/docs/examples/queues.ts +254 -0
  114. package/docs/examples/rate-limiting.ts +42 -0
  115. package/docs/examples/redis.ts +99 -0
  116. package/docs/examples/request-response.ts +197 -0
  117. package/docs/examples/routing.ts +186 -0
  118. package/docs/examples/scheduling.ts +62 -0
  119. package/docs/examples/sessions.ts +102 -0
  120. package/docs/examples/static-files.ts +63 -0
  121. package/docs/examples/storage.ts +132 -0
  122. package/docs/examples/templates.ts +58 -0
  123. package/docs/examples/testing.ts +66 -0
  124. package/docs/examples/transformer.ts +141 -0
  125. package/docs/examples/transformers.ts +49 -0
  126. package/docs/examples/url-builder.ts +86 -0
  127. package/docs/examples/validation.ts +102 -0
  128. package/docs/examples/views.tsx +62 -0
  129. package/docs/examples/vite.ts +106 -0
  130. package/docs/factories.md +166 -0
  131. package/docs/getting-started.md +290 -0
  132. package/docs/hashing.md +259 -0
  133. package/docs/health.md +225 -0
  134. package/docs/helpers.md +347 -0
  135. package/docs/hono.md +186 -0
  136. package/docs/hooks.md +118 -0
  137. package/docs/inertia.md +241 -0
  138. package/docs/locks.md +323 -0
  139. package/docs/logger.md +290 -0
  140. package/docs/mail.md +678 -0
  141. package/docs/middleware.md +425 -0
  142. package/docs/migrations.md +476 -0
  143. package/docs/models.md +810 -0
  144. package/docs/notifications.md +474 -0
  145. package/docs/providers.md +363 -0
  146. package/docs/queues.md +679 -0
  147. package/docs/rate-limiting.md +155 -0
  148. package/docs/redis.md +178 -0
  149. package/docs/request-response.md +953 -0
  150. package/docs/routing.md +804 -0
  151. package/docs/scheduling.md +110 -0
  152. package/docs/security.md +85 -0
  153. package/docs/sessions.md +354 -0
  154. package/docs/social-auth.md +174 -0
  155. package/docs/static-files.md +211 -0
  156. package/docs/storage.md +450 -0
  157. package/docs/templates.md +315 -0
  158. package/docs/testing.md +125 -0
  159. package/docs/transformers.md +381 -0
  160. package/docs/url-builder.md +295 -0
  161. package/docs/validation.md +288 -0
  162. package/docs/views.md +267 -0
  163. package/docs/vite.md +434 -0
  164. package/llms-full.txt +17694 -0
  165. package/llms.txt +116 -0
  166. package/package.json +38 -7
@@ -5,9 +5,27 @@
5
5
  *
6
6
  * listen("user.registered", (user) => sendWelcome(user));
7
7
  * await emit("user.registered", user);
8
+ *
9
+ * Declare an event in `EventsList` and the emitter checks both sides of it —
10
+ * the payload you fire and the payload your listener receives:
11
+ *
12
+ * declare module "@shaferllc/keel/core" {
13
+ * interface EventsList {
14
+ * "order.paid": { id: number; total: number };
15
+ * }
16
+ * }
17
+ *
18
+ * One listener's failure never stops another's: `emit` runs them all, then
19
+ * reports what broke — to `onError()` if you registered one, otherwise by
20
+ * rejecting.
8
21
  */
9
22
  export class Events {
10
23
  listeners = new Map();
24
+ anyListeners = new Set();
25
+ errorHandler;
26
+ /** Non-null while faking: the events to intercept (`true` = all of them). */
27
+ faked;
28
+ buffer;
11
29
  /** Subscribe to an event. Returns an unsubscribe function. */
12
30
  on(event, listener) {
13
31
  const set = this.listeners.get(event) ?? new Set();
@@ -18,7 +36,9 @@ export class Events {
18
36
  /** Subscribe for a single emission. */
19
37
  once(event, listener) {
20
38
  const wrapper = async (payload) => {
21
- this.off(event, wrapper);
39
+ // Unsubscribe before awaiting, so a listener that re-emits the same event
40
+ // doesn't re-trigger this one.
41
+ this.listeners.get(event)?.delete(wrapper);
22
42
  await listener(payload);
23
43
  };
24
44
  return this.on(event, wrapper);
@@ -26,13 +46,66 @@ export class Events {
26
46
  off(event, listener) {
27
47
  this.listeners.get(event)?.delete(listener);
28
48
  }
29
- /** Emit an event, awaiting every listener in order. */
30
- async emit(event, payload) {
31
- const set = this.listeners.get(event);
32
- if (!set)
49
+ /**
50
+ * Subscribe to *every* event for logging and other cross-cutting concerns.
51
+ * These run before the event's own listeners. Returns an unsubscribe function.
52
+ */
53
+ onAny(listener) {
54
+ this.anyListeners.add(listener);
55
+ return () => {
56
+ this.anyListeners.delete(listener);
57
+ };
58
+ }
59
+ /**
60
+ * Handle listener failures instead of letting `emit` reject. Without one, an
61
+ * `emit` whose listeners threw rejects (with an `AggregateError` if more than
62
+ * one did) — errors are never silently swallowed.
63
+ */
64
+ onError(handler) {
65
+ this.errorHandler = handler;
66
+ }
67
+ /**
68
+ * Emit an event, awaiting every listener in registration order.
69
+ *
70
+ * A listener that throws does not stop the others — they all run, and the
71
+ * failures are reported afterwards. That's the point of an emitter: one
72
+ * subscriber's bug shouldn't silently cancel an unrelated subscriber's work.
73
+ */
74
+ async emit(event, ...args) {
75
+ const payload = args[0];
76
+ if (this.faked && (this.faked === true || this.faked.has(event))) {
77
+ this.buffer?.record(event, payload);
33
78
  return;
34
- for (const listener of [...set])
35
- await listener(payload);
79
+ }
80
+ const errors = [];
81
+ // Snapshot both sets: a listener that (un)subscribes mid-emit affects only
82
+ // the next emission, not this one.
83
+ for (const listener of [...this.anyListeners]) {
84
+ try {
85
+ await listener(event, payload);
86
+ }
87
+ catch (error) {
88
+ errors.push(error);
89
+ }
90
+ }
91
+ for (const listener of [...(this.listeners.get(event) ?? [])]) {
92
+ try {
93
+ await listener(payload);
94
+ }
95
+ catch (error) {
96
+ errors.push(error);
97
+ }
98
+ }
99
+ if (!errors.length)
100
+ return;
101
+ if (this.errorHandler) {
102
+ for (const error of errors)
103
+ await this.errorHandler(event, error, payload);
104
+ return;
105
+ }
106
+ if (errors.length === 1)
107
+ throw errors[0];
108
+ throw new AggregateError(errors, `${errors.length} listeners for "${event}" failed.`);
36
109
  }
37
110
  listenerCount(event) {
38
111
  return this.listeners.get(event)?.size ?? 0;
@@ -43,4 +116,89 @@ export class Events {
43
116
  else
44
117
  this.listeners.clear();
45
118
  }
119
+ /** Drop every `onAny` listener and the error handler too. */
120
+ clearAll() {
121
+ this.listeners.clear();
122
+ this.anyListeners.clear();
123
+ this.errorHandler = undefined;
124
+ }
125
+ /**
126
+ * Record emissions instead of running listeners, so a test can assert an event
127
+ * fired without triggering its side effects. Undo with `restore()`.
128
+ *
129
+ * const events = events().fake();
130
+ * await register(user);
131
+ * events.assertEmitted("user.registered");
132
+ *
133
+ * Pass one or more event names to fake only those — everything else dispatches
134
+ * for real.
135
+ */
136
+ fake(only) {
137
+ this.faked = only === undefined ? true : new Set(Array.isArray(only) ? only : [only]);
138
+ this.buffer = new EventBuffer();
139
+ return this.buffer;
140
+ }
141
+ /** Stop faking; listeners run for real again. */
142
+ restore() {
143
+ this.faked = undefined;
144
+ this.buffer = undefined;
145
+ }
146
+ }
147
+ /* ------------------------------ event buffer ------------------------------ */
148
+ /** What a fake records, plus assertions over it. */
149
+ export class EventBuffer {
150
+ events = [];
151
+ /** @internal — called by `Events.emit` while faking. */
152
+ record(event, payload) {
153
+ this.events.push({ event, payload });
154
+ }
155
+ /** Every emission recorded so far, in order. */
156
+ all() {
157
+ return [...this.events];
158
+ }
159
+ /** The payloads recorded for one event. */
160
+ payloadsFor(event) {
161
+ return this.events.filter((e) => e.event === event).map((e) => e.payload);
162
+ }
163
+ /**
164
+ * Assert the event fired — optionally only counting emissions whose payload
165
+ * satisfies `predicate`.
166
+ *
167
+ * buffer.assertEmitted("order.paid", (o) => o.total === 4200);
168
+ */
169
+ assertEmitted(event, predicate) {
170
+ const matches = this.payloadsFor(event).filter((p) => predicate?.(p) ?? true);
171
+ if (matches.length)
172
+ return;
173
+ const fired = this.listenerCountFor(event);
174
+ throw new Error(predicate && fired
175
+ ? `Expected "${event}" to be emitted with a matching payload. It fired ${fired} time(s), but none matched.`
176
+ : `Expected "${event}" to be emitted, but it was not. ${this.summary()}`);
177
+ }
178
+ assertNotEmitted(event) {
179
+ const fired = this.listenerCountFor(event);
180
+ if (fired)
181
+ throw new Error(`Expected "${event}" not to be emitted, but it fired ${fired} time(s).`);
182
+ }
183
+ assertEmittedCount(event, expected) {
184
+ const fired = this.listenerCountFor(event);
185
+ if (fired !== expected) {
186
+ throw new Error(`Expected "${event}" to be emitted ${expected} time(s), but it fired ${fired}.`);
187
+ }
188
+ }
189
+ /** Assert nothing at all was emitted. */
190
+ assertNoneEmitted() {
191
+ if (this.events.length) {
192
+ throw new Error(`Expected no events, but ${this.events.length} fired. ${this.summary()}`);
193
+ }
194
+ }
195
+ listenerCountFor(event) {
196
+ return this.events.filter((e) => e.event === event).length;
197
+ }
198
+ summary() {
199
+ if (!this.events.length)
200
+ return "No events were emitted.";
201
+ const names = [...new Set(this.events.map((e) => e.event))].join(", ");
202
+ return `Emitted: ${names}.`;
203
+ }
46
204
  }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Health checks — the two endpoints an orchestrator (Kubernetes, Fly, Railway,
3
+ * a load balancer) asks about before it sends you traffic:
4
+ *
5
+ * /health/live is the process up? — answers instantly, checks nothing
6
+ * /health/ready can it serve requests? — runs every registered check
7
+ *
8
+ * A liveness probe that touched the database would restart a healthy app during a
9
+ * database blip, so it deliberately checks nothing. Readiness is where the checks
10
+ * live: it reports 200 while everything it depends on is reachable, and 503 when
11
+ * something isn't — which pulls the instance out of the pool without killing it.
12
+ *
13
+ * health().register([new DatabaseCheck(), new RedisCheck()]);
14
+ * this.use(healthCheck()); // serves both endpoints
15
+ *
16
+ * Keel ships the checks that mean something wherever it runs — a database, Redis,
17
+ * the cache. Deliberately absent are AdonisJS's disk-space and heap/RSS checks:
18
+ * they measure a Node process, and on Workers there isn't one.
19
+ */
20
+ import type { MiddlewareHandler } from "hono";
21
+ export type HealthStatus = "ok" | "warning" | "error";
22
+ /** The outcome of a single check. Build one with `Result.ok/warning/failed`. */
23
+ export declare class Result {
24
+ readonly status: HealthStatus;
25
+ readonly message: string;
26
+ readonly error?: unknown | undefined;
27
+ meta?: Record<string, unknown> | undefined;
28
+ private constructor();
29
+ /** Healthy. */
30
+ static ok(message: string): Result;
31
+ /**
32
+ * Working, but degraded — worth paging someone, not worth pulling the instance
33
+ * out of the pool. A warning keeps readiness at 200.
34
+ */
35
+ static warning(message: string): Result;
36
+ /** Broken. Any failed check takes readiness to 503. */
37
+ static failed(message: string, error?: unknown): Result;
38
+ /** Attach arbitrary detail — connection counts, latencies, versions. */
39
+ withMeta(meta: Record<string, unknown>): this;
40
+ }
41
+ /** One thing worth knowing about before serving traffic. */
42
+ export declare abstract class BaseCheck {
43
+ abstract readonly name: string;
44
+ private ttl;
45
+ private cached?;
46
+ abstract run(): Promise<Result>;
47
+ /**
48
+ * Reuse this check's last result for `seconds`, so a probe every few seconds
49
+ * doesn't hammer the thing it's checking. The report marks a reused result
50
+ * with `isCached: true`.
51
+ */
52
+ cacheFor(seconds: number): this;
53
+ /**
54
+ * Run the check, honouring `cacheFor`, and never throw: a check that blows up
55
+ * is itself a failure, and one broken check must not take down the report.
56
+ *
57
+ * @internal — called by `HealthChecks.run()`.
58
+ */
59
+ execute(): Promise<{
60
+ result: Result;
61
+ isCached: boolean;
62
+ }>;
63
+ }
64
+ /**
65
+ * A check from a plain function — the escape hatch, for anything Keel doesn't
66
+ * ship a check for.
67
+ *
68
+ * health().register([
69
+ * check("stripe", async () => {
70
+ * const res = await fetch("https://api.stripe.com/healthcheck");
71
+ * return res.ok ? Result.ok("Stripe is reachable") : Result.failed("Stripe is down");
72
+ * }),
73
+ * ]);
74
+ */
75
+ export declare function check(name: string, run: () => Promise<Result> | Result): BaseCheck;
76
+ /** Is the database reachable? Runs `SELECT 1` on a connection. */
77
+ export declare class DatabaseCheck extends BaseCheck {
78
+ private connectionName?;
79
+ readonly name: string;
80
+ constructor(connectionName?: string | undefined);
81
+ run(): Promise<Result>;
82
+ }
83
+ /** Is Redis reachable? Reads a key — a failed read means a broken connection. */
84
+ export declare class RedisCheck extends BaseCheck {
85
+ readonly name = "redis";
86
+ run(): Promise<Result>;
87
+ }
88
+ /** Does the cache round-trip? Writes a key, reads it back, deletes it. */
89
+ export declare class CacheCheck extends BaseCheck {
90
+ readonly name = "cache";
91
+ run(): Promise<Result>;
92
+ }
93
+ export interface CheckReport {
94
+ name: string;
95
+ status: HealthStatus;
96
+ message: string;
97
+ /** Whether this result was reused from a `cacheFor()` window. */
98
+ isCached: boolean;
99
+ finishedAt: string;
100
+ meta?: Record<string, unknown>;
101
+ }
102
+ export interface HealthReport {
103
+ /** True unless a check failed. A warning is still healthy. */
104
+ isHealthy: boolean;
105
+ /** The worst status any check reported. */
106
+ status: HealthStatus;
107
+ finishedAt: string;
108
+ checks: CheckReport[];
109
+ }
110
+ export declare class HealthChecks {
111
+ private checks;
112
+ /** Add checks. Call it once, in a provider's `boot()`. */
113
+ register(checks: BaseCheck[]): this;
114
+ /** Every registered check. */
115
+ all(): BaseCheck[];
116
+ clear(): this;
117
+ /** Run every check — concurrently, since they're independent I/O. */
118
+ run(): Promise<HealthReport>;
119
+ }
120
+ /** The application's health-check registry. */
121
+ export declare function health(): HealthChecks;
122
+ export interface HealthCheckOptions {
123
+ /** URL prefix for the two endpoints. Default: `"/health"`. */
124
+ basePath?: string;
125
+ /**
126
+ * Require `Authorization: Bearer <secret>` on the readiness endpoint. The
127
+ * report names your infrastructure, so don't publish it — set this whenever
128
+ * the endpoint is reachable from outside your network.
129
+ */
130
+ secret?: string;
131
+ /** The registry to run. Defaults to the global one from `health()`. */
132
+ checks?: HealthChecks;
133
+ }
134
+ /**
135
+ * Serve `/health/live` and `/health/ready`. Anything else falls through to your
136
+ * routes.
137
+ *
138
+ * this.use(healthCheck());
139
+ * this.use(healthCheck({ secret: env.HEALTH_SECRET }));
140
+ */
141
+ export declare function healthCheck(options?: HealthCheckOptions): MiddlewareHandler;
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Health checks — the two endpoints an orchestrator (Kubernetes, Fly, Railway,
3
+ * a load balancer) asks about before it sends you traffic:
4
+ *
5
+ * /health/live is the process up? — answers instantly, checks nothing
6
+ * /health/ready can it serve requests? — runs every registered check
7
+ *
8
+ * A liveness probe that touched the database would restart a healthy app during a
9
+ * database blip, so it deliberately checks nothing. Readiness is where the checks
10
+ * live: it reports 200 while everything it depends on is reachable, and 503 when
11
+ * something isn't — which pulls the instance out of the pool without killing it.
12
+ *
13
+ * health().register([new DatabaseCheck(), new RedisCheck()]);
14
+ * this.use(healthCheck()); // serves both endpoints
15
+ *
16
+ * Keel ships the checks that mean something wherever it runs — a database, Redis,
17
+ * the cache. Deliberately absent are AdonisJS's disk-space and heap/RSS checks:
18
+ * they measure a Node process, and on Workers there isn't one.
19
+ */
20
+ import { connection } from "./database.js";
21
+ import { redis } from "./redis.js";
22
+ import { cache } from "./helpers.js";
23
+ /** The outcome of a single check. Build one with `Result.ok/warning/failed`. */
24
+ export class Result {
25
+ status;
26
+ message;
27
+ error;
28
+ meta;
29
+ constructor(status, message, error, meta) {
30
+ this.status = status;
31
+ this.message = message;
32
+ this.error = error;
33
+ this.meta = meta;
34
+ }
35
+ /** Healthy. */
36
+ static ok(message) {
37
+ return new Result("ok", message);
38
+ }
39
+ /**
40
+ * Working, but degraded — worth paging someone, not worth pulling the instance
41
+ * out of the pool. A warning keeps readiness at 200.
42
+ */
43
+ static warning(message) {
44
+ return new Result("warning", message);
45
+ }
46
+ /** Broken. Any failed check takes readiness to 503. */
47
+ static failed(message, error) {
48
+ return new Result("error", message, error);
49
+ }
50
+ /** Attach arbitrary detail — connection counts, latencies, versions. */
51
+ withMeta(meta) {
52
+ this.meta = { ...this.meta, ...meta };
53
+ return this;
54
+ }
55
+ }
56
+ /* --------------------------------- checks --------------------------------- */
57
+ /** One thing worth knowing about before serving traffic. */
58
+ export class BaseCheck {
59
+ ttl = 0;
60
+ cached;
61
+ /**
62
+ * Reuse this check's last result for `seconds`, so a probe every few seconds
63
+ * doesn't hammer the thing it's checking. The report marks a reused result
64
+ * with `isCached: true`.
65
+ */
66
+ cacheFor(seconds) {
67
+ this.ttl = seconds;
68
+ return this;
69
+ }
70
+ /**
71
+ * Run the check, honouring `cacheFor`, and never throw: a check that blows up
72
+ * is itself a failure, and one broken check must not take down the report.
73
+ *
74
+ * @internal — called by `HealthChecks.run()`.
75
+ */
76
+ async execute() {
77
+ const now = Date.now();
78
+ if (this.cached && this.cached.expiresAt > now) {
79
+ return { result: this.cached.result, isCached: true };
80
+ }
81
+ let result;
82
+ try {
83
+ result = await this.run();
84
+ }
85
+ catch (error) {
86
+ result = Result.failed(error instanceof Error ? error.message : `The "${this.name}" check threw.`, error);
87
+ }
88
+ if (this.ttl > 0) {
89
+ this.cached = { result, expiresAt: now + this.ttl * 1000 };
90
+ }
91
+ return { result, isCached: false };
92
+ }
93
+ }
94
+ /**
95
+ * A check from a plain function — the escape hatch, for anything Keel doesn't
96
+ * ship a check for.
97
+ *
98
+ * health().register([
99
+ * check("stripe", async () => {
100
+ * const res = await fetch("https://api.stripe.com/healthcheck");
101
+ * return res.ok ? Result.ok("Stripe is reachable") : Result.failed("Stripe is down");
102
+ * }),
103
+ * ]);
104
+ */
105
+ export function check(name, run) {
106
+ return new (class extends BaseCheck {
107
+ name = name;
108
+ async run() {
109
+ return run();
110
+ }
111
+ })();
112
+ }
113
+ /** Is the database reachable? Runs `SELECT 1` on a connection. */
114
+ export class DatabaseCheck extends BaseCheck {
115
+ connectionName;
116
+ name;
117
+ constructor(connectionName) {
118
+ super();
119
+ this.connectionName = connectionName;
120
+ this.name = connectionName ? `database (${connectionName})` : "database";
121
+ }
122
+ async run() {
123
+ const started = Date.now();
124
+ await connection(this.connectionName).select("SELECT 1");
125
+ return Result.ok("Database is reachable").withMeta({ durationMs: Date.now() - started });
126
+ }
127
+ }
128
+ /** Is Redis reachable? Reads a key — a failed read means a broken connection. */
129
+ export class RedisCheck extends BaseCheck {
130
+ name = "redis";
131
+ async run() {
132
+ const started = Date.now();
133
+ await redis().get("keel:health");
134
+ return Result.ok("Redis is reachable").withMeta({ durationMs: Date.now() - started });
135
+ }
136
+ }
137
+ /** Does the cache round-trip? Writes a key, reads it back, deletes it. */
138
+ export class CacheCheck extends BaseCheck {
139
+ name = "cache";
140
+ async run() {
141
+ const started = Date.now();
142
+ const key = "keel:health";
143
+ const store = cache();
144
+ await store.put(key, "ok", 10);
145
+ const value = await store.get(key);
146
+ await store.forget(key);
147
+ if (value !== "ok") {
148
+ return Result.failed("The cache did not return the value just written to it.");
149
+ }
150
+ return Result.ok("Cache is reachable").withMeta({ durationMs: Date.now() - started });
151
+ }
152
+ }
153
+ /* ------------------------------- the registry ------------------------------ */
154
+ export class HealthChecks {
155
+ checks = [];
156
+ /** Add checks. Call it once, in a provider's `boot()`. */
157
+ register(checks) {
158
+ this.checks.push(...checks);
159
+ return this;
160
+ }
161
+ /** Every registered check. */
162
+ all() {
163
+ return [...this.checks];
164
+ }
165
+ clear() {
166
+ this.checks = [];
167
+ return this;
168
+ }
169
+ /** Run every check — concurrently, since they're independent I/O. */
170
+ async run() {
171
+ const results = await Promise.all(this.checks.map(async (c) => {
172
+ const { result, isCached } = await c.execute();
173
+ return {
174
+ name: c.name,
175
+ status: result.status,
176
+ message: result.message,
177
+ isCached,
178
+ finishedAt: new Date().toISOString(),
179
+ ...(result.meta ? { meta: result.meta } : {}),
180
+ };
181
+ }));
182
+ const status = results.some((r) => r.status === "error")
183
+ ? "error"
184
+ : results.some((r) => r.status === "warning")
185
+ ? "warning"
186
+ : "ok";
187
+ return {
188
+ isHealthy: status !== "error",
189
+ status,
190
+ finishedAt: new Date().toISOString(),
191
+ checks: results,
192
+ };
193
+ }
194
+ }
195
+ const registry = new HealthChecks();
196
+ /** The application's health-check registry. */
197
+ export function health() {
198
+ return registry;
199
+ }
200
+ /**
201
+ * Serve `/health/live` and `/health/ready`. Anything else falls through to your
202
+ * routes.
203
+ *
204
+ * this.use(healthCheck());
205
+ * this.use(healthCheck({ secret: env.HEALTH_SECRET }));
206
+ */
207
+ export function healthCheck(options = {}) {
208
+ const basePath = (options.basePath ?? "/health").replace(/\/+$/, "");
209
+ return async (c, next) => {
210
+ if (c.req.method !== "GET" && c.req.method !== "HEAD")
211
+ return next();
212
+ const { pathname } = new URL(c.req.url);
213
+ // Liveness: the process answered, which is the whole question. Checking a
214
+ // dependency here would get a healthy app restarted during a database blip.
215
+ if (pathname === `${basePath}/live`) {
216
+ return c.json({ isHealthy: true, status: "ok" });
217
+ }
218
+ if (pathname !== `${basePath}/ready`)
219
+ return next();
220
+ if (options.secret && c.req.header("Authorization") !== `Bearer ${options.secret}`) {
221
+ return c.json({ message: "Unauthorized" }, 401);
222
+ }
223
+ const report = await (options.checks ?? registry).run();
224
+ return c.json(report, report.isHealthy ? 200 : 503);
225
+ };
226
+ }
@@ -10,11 +10,17 @@
10
10
  import type { Application } from "./application.js";
11
11
  import type { Token, Factory } from "./container.js";
12
12
  import { type Renderable } from "./view.js";
13
- import { Events, type Listener } from "./events.js";
13
+ import { Events, type Listener, type EventName, type EmitArgs, type Resolve } from "./events.js";
14
14
  import { Cache } from "./cache.js";
15
15
  import { Logger } from "./logger.js";
16
16
  /** Register the active application. Called by the Application constructor. */
17
17
  export declare function setApplication(app: Application): void;
18
+ /**
19
+ * Whether an application has been bootstrapped. Primitives that must work
20
+ * without one — a queue worker, a mailer in a unit test — check this before
21
+ * reaching for `logger()` or `emit()`, both of which throw when there's no app.
22
+ */
23
+ export declare function hasApplication(): boolean;
18
24
  /** The active application container. Throws if none has been created. */
19
25
  export declare function app(): Application;
20
26
  /** Run a callback once the active application has booted (see `Application.onReady`). */
@@ -38,12 +44,18 @@ export declare function instance<T>(token: Token<T>, value: T): T;
38
44
  export declare function make<T>(token: Token<T>): T;
39
45
  /** Whether a token is bound or has a cached instance. */
40
46
  export declare function bound(token: Token): boolean;
47
+ /** Register an alias token that resolves to another token. */
48
+ export declare function alias<T>(aliasToken: Token<T>, target: Token<T>): void;
49
+ /** Temporarily replace a binding with a fake (tests). Undo with `restore()`. */
50
+ export declare function swap<T>(token: Token<T>, factory: Factory<T>): void;
51
+ /** Undo a `swap()` — restore the original binding. No token restores every swap. */
52
+ export declare function restore(token?: Token): void;
41
53
  /** The application's event emitter. */
42
54
  export declare function events(): Events;
43
55
  /** Emit an event, awaiting every listener. */
44
- export declare function emit<T = unknown>(event: string, payload?: T): Promise<void>;
56
+ export declare function emit<T = unknown, E extends EventName = EventName>(event: E, ...args: EmitArgs<Resolve<T, E>>): Promise<void>;
45
57
  /** Subscribe to an event; returns an unsubscribe function. */
46
- export declare function listen<T = unknown>(event: string, listener: Listener<T>): () => void;
58
+ export declare function listen<T = unknown, E extends EventName = EventName>(event: E, listener: Listener<Resolve<T, E>>): () => void;
47
59
  /** The application's cache. */
48
60
  export declare function cache(): Cache;
49
61
  /** The application's logger. */
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { Config } from "./config.js";
11
11
  import { View } from "./view.js";
12
- import { Events } from "./events.js";
12
+ import { Events, } from "./events.js";
13
13
  import { Cache } from "./cache.js";
14
14
  import { Logger } from "./logger.js";
15
15
  let current;
@@ -17,6 +17,14 @@ let current;
17
17
  export function setApplication(app) {
18
18
  current = app;
19
19
  }
20
+ /**
21
+ * Whether an application has been bootstrapped. Primitives that must work
22
+ * without one — a queue worker, a mailer in a unit test — check this before
23
+ * reaching for `logger()` or `emit()`, both of which throw when there's no app.
24
+ */
25
+ export function hasApplication() {
26
+ return current !== undefined;
27
+ }
20
28
  /** The active application container. Throws if none has been created. */
21
29
  export function app() {
22
30
  if (!current) {
@@ -65,14 +73,26 @@ export function make(token) {
65
73
  export function bound(token) {
66
74
  return app().bound(token);
67
75
  }
76
+ /** Register an alias token that resolves to another token. */
77
+ export function alias(aliasToken, target) {
78
+ app().alias(aliasToken, target);
79
+ }
80
+ /** Temporarily replace a binding with a fake (tests). Undo with `restore()`. */
81
+ export function swap(token, factory) {
82
+ app().swap(token, factory);
83
+ }
84
+ /** Undo a `swap()` — restore the original binding. No token restores every swap. */
85
+ export function restore(token) {
86
+ app().restore(token);
87
+ }
68
88
  /* ------------------------------- events -------------------------------- */
69
89
  /** The application's event emitter. */
70
90
  export function events() {
71
91
  return app().make(Events);
72
92
  }
73
93
  /** Emit an event, awaiting every listener. */
74
- export function emit(event, payload) {
75
- return events().emit(event, payload);
94
+ export function emit(event, ...args) {
95
+ return events().emit(event, ...args);
76
96
  }
77
97
  /** Subscribe to an event; returns an unsubscribe function. */
78
98
  export function listen(event, listener) {