@beignet/core 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @beignet/core
2
2
 
3
+ ## 0.0.22
4
+
5
+ ### Patch Changes
6
+
7
+ - Add app-owned readiness helpers, first-party provider health checks, generated `/api/ready` routes, and doctor readiness hints.
8
+
3
9
  ## 0.0.21
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -1269,6 +1269,32 @@ The public adapter contract is `HttpAdapter<NativeRequest, NativeResponse>`.
1269
1269
  Use it when building a runtime package beyond the first-party `@beignet/web`
1270
1270
  and `@beignet/next` adapters.
1271
1271
 
1272
+ ### Health and readiness
1273
+
1274
+ Use `createHealthHandler(...)` and `runHealthChecks(...)` from
1275
+ `@beignet/core/server` for app-owned liveness and readiness endpoints.
1276
+ Readiness checks should be cheap, bounded, and non-mutating:
1277
+
1278
+ ```ts
1279
+ import { createHealthHandler } from "@beignet/core/server";
1280
+ import { server } from "@/server";
1281
+
1282
+ const readiness = createHealthHandler(
1283
+ server.ports,
1284
+ {
1285
+ checks: {
1286
+ database: (ports) => ports.db.checkHealth(),
1287
+ },
1288
+ timeoutMs: 2000,
1289
+ },
1290
+ "production",
1291
+ );
1292
+ ```
1293
+
1294
+ Provider checks such as `ctx.ports.db.checkHealth()` should be called from
1295
+ routes, workers, or deployment probes. Do not run migrations, drains, workers,
1296
+ or polling loops from health checks.
1297
+
1272
1298
  ### Request instrumentation and tracing
1273
1299
 
1274
1300
  `createServer(...)` owns request instrumentation. For every request it
@@ -4,6 +4,27 @@
4
4
  */
5
5
  import type { AnyPorts } from "../ports/index.js";
6
6
  import type { HttpRequestLike, HttpResponseLike } from "./types.js";
7
+ /**
8
+ * Per-dependency health detail returned by readiness checks.
9
+ */
10
+ export interface HealthCheckDetail {
11
+ /**
12
+ * Whether this dependency is healthy.
13
+ */
14
+ ok: boolean;
15
+ /**
16
+ * Optional human-readable status. Avoid secrets and raw provider credentials.
17
+ */
18
+ message?: string;
19
+ /**
20
+ * Milliseconds spent running this dependency check.
21
+ */
22
+ durationMs?: number;
23
+ /**
24
+ * Optional safe metadata for operators.
25
+ */
26
+ metadata?: Record<string, unknown>;
27
+ }
7
28
  /**
8
29
  * Health check result returned by health handlers.
9
30
  */
@@ -15,10 +36,33 @@ export interface HealthCheckResult {
15
36
  /**
16
37
  * Optional per-dependency health details.
17
38
  */
18
- details?: Record<string, {
19
- ok: boolean;
20
- message?: string;
21
- }>;
39
+ details?: Record<string, HealthCheckDetail>;
40
+ }
41
+ /**
42
+ * A named dependency check run by {@link runHealthChecks}.
43
+ */
44
+ export type HealthCheck<Ports> = (ports: Ports) => Promise<HealthCheckDetail | boolean | undefined> | HealthCheckDetail | boolean | undefined;
45
+ /**
46
+ * Named dependency checks for an app-owned readiness endpoint.
47
+ */
48
+ export type HealthChecks<Ports> = Record<string, HealthCheck<Ports>>;
49
+ /**
50
+ * Options for running named dependency checks.
51
+ */
52
+ export interface RunHealthChecksOptions {
53
+ /**
54
+ * Maximum time to wait for each dependency check.
55
+ *
56
+ * Defaults to 2000ms.
57
+ */
58
+ timeoutMs?: number;
59
+ /**
60
+ * Include thrown error messages in dependency details.
61
+ *
62
+ * Defaults to true. Set false in production responses when provider errors
63
+ * may include sensitive details.
64
+ */
65
+ includeErrorDetails?: boolean;
22
66
  }
23
67
  /**
24
68
  * Health check configuration.
@@ -34,11 +78,24 @@ export interface HealthConfig<Ports> {
34
78
  suggestedPath?: string;
35
79
  /** Custom health check function */
36
80
  check?: (ports: Ports) => Promise<HealthCheckResult>;
81
+ /** Named dependency checks for an app-owned readiness endpoint */
82
+ checks?: HealthChecks<Ports>;
83
+ /** Per-check timeout for named dependency checks */
84
+ timeoutMs?: number;
37
85
  }
38
86
  /**
39
87
  * Application environment.
40
88
  */
41
89
  export type AppEnvironment = "development" | "production" | "test";
90
+ /**
91
+ * Run named dependency health checks in parallel and aggregate the result.
92
+ *
93
+ * This is intended for app-owned readiness endpoints. Checks should be cheap,
94
+ * bounded, non-mutating probes such as `SELECT 1`, Redis `PING`, or provider
95
+ * health endpoints. Do not start workers, drains, migrations, or polling loops
96
+ * from readiness checks.
97
+ */
98
+ export declare function runHealthChecks<Ports extends AnyPorts>(ports: Ports, checks: HealthChecks<Ports>, options?: RunHealthChecksOptions): Promise<HealthCheckResult>;
42
99
  /**
43
100
  * Create a framework-neutral health check handler.
44
101
  *
@@ -1 +1 @@
1
- {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,EAAE,EAAE,OAAO,CAAC;IACZ;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC7D;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,KAAK;IACjC,8CAA8C;IAC9C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mCAAmC;IACnC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACtD;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,YAAY,GAAG,MAAM,CAAC;AAEnE;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,SAAS,QAAQ,EACxD,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,SAAS,EAC7C,GAAG,EAAE,cAAc,GAClB,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAoCrD"}
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,EAAE,EAAE,OAAO,CAAC;IACZ;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,EAAE,EAAE,OAAO,CAAC;IACZ;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC7C;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,KAAK,IAAI,CAC/B,KAAK,EAAE,KAAK,KAEV,OAAO,CAAC,iBAAiB,GAAG,OAAO,GAAG,SAAS,CAAC,GAChD,iBAAiB,GACjB,OAAO,GACP,SAAS,CAAC;AAEd;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,KAAK;IACjC,8CAA8C;IAC9C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mCAAmC;IACnC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACrD,kEAAkE;IAClE,MAAM,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IAC7B,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,YAAY,GAAG,MAAM,CAAC;AAyDnE;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,KAAK,SAAS,QAAQ,EAC1D,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,EAC3B,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CA6C5B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,SAAS,QAAQ,EACxD,KAAK,EAAE,KAAK,EACZ,YAAY,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,SAAS,EAC7C,GAAG,EAAE,cAAc,GAClB,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAyCrD"}
@@ -2,6 +2,84 @@
2
2
  * Health check handler
3
3
  * Health check handler for Beignet server adapters.
4
4
  */
5
+ const DEFAULT_HEALTH_TIMEOUT_MS = 2000;
6
+ function errorMessage(error) {
7
+ return error instanceof Error ? error.message : String(error);
8
+ }
9
+ function assertTimeout(timeoutMs) {
10
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
11
+ throw new Error("Health check timeoutMs must be a positive integer.");
12
+ }
13
+ }
14
+ function normalizeHealthDetail(result) {
15
+ if (typeof result === "boolean")
16
+ return { ok: result };
17
+ return result ?? { ok: true };
18
+ }
19
+ function redactHealthDetail(detail, includeErrorDetails) {
20
+ if (includeErrorDetails || detail.ok || detail.message === undefined) {
21
+ return detail;
22
+ }
23
+ return {
24
+ ...detail,
25
+ message: "Health check failed",
26
+ };
27
+ }
28
+ function withTimeout(promise, timeoutMs, name) {
29
+ let timeout;
30
+ const timeoutPromise = new Promise((_, reject) => {
31
+ timeout = setTimeout(() => {
32
+ reject(new Error(`Health check "${name}" did not complete within ${timeoutMs}ms.`));
33
+ }, timeoutMs);
34
+ });
35
+ return Promise.race([promise, timeoutPromise]).finally(() => {
36
+ if (timeout)
37
+ clearTimeout(timeout);
38
+ });
39
+ }
40
+ /**
41
+ * Run named dependency health checks in parallel and aggregate the result.
42
+ *
43
+ * This is intended for app-owned readiness endpoints. Checks should be cheap,
44
+ * bounded, non-mutating probes such as `SELECT 1`, Redis `PING`, or provider
45
+ * health endpoints. Do not start workers, drains, migrations, or polling loops
46
+ * from readiness checks.
47
+ */
48
+ export async function runHealthChecks(ports, checks, options = {}) {
49
+ const timeoutMs = options.timeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
50
+ assertTimeout(timeoutMs);
51
+ const includeErrorDetails = options.includeErrorDetails ?? true;
52
+ const entries = await Promise.all(Object.entries(checks).map(async ([name, check]) => {
53
+ const startedAt = Date.now();
54
+ try {
55
+ const detail = redactHealthDetail(normalizeHealthDetail(await withTimeout(Promise.resolve(check(ports)), timeoutMs, name)), includeErrorDetails);
56
+ return [
57
+ name,
58
+ {
59
+ ...detail,
60
+ durationMs: Date.now() - startedAt,
61
+ },
62
+ ];
63
+ }
64
+ catch (error) {
65
+ return [
66
+ name,
67
+ {
68
+ ok: false,
69
+ message: includeErrorDetails
70
+ ? errorMessage(error)
71
+ : "Health check failed",
72
+ durationMs: Date.now() - startedAt,
73
+ },
74
+ ];
75
+ }
76
+ }));
77
+ const details = Object.fromEntries(entries);
78
+ return {
79
+ ok: Object.values(details).every((detail) => detail.ok),
80
+ details,
81
+ };
82
+ }
5
83
  /**
6
84
  * Create a framework-neutral health check handler.
7
85
  *
@@ -35,6 +113,12 @@ export function createHealthHandler(ports, healthConfig, env) {
35
113
  };
36
114
  }
37
115
  }
116
+ else if (healthConfig?.checks) {
117
+ result = await runHealthChecks(ports, healthConfig.checks, {
118
+ timeoutMs: healthConfig.timeoutMs,
119
+ includeErrorDetails: env !== "production",
120
+ });
121
+ }
38
122
  else {
39
123
  result = { ok: true };
40
124
  }
@@ -1 +1 @@
1
- {"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAwCH;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAY,EACZ,YAA6C,EAC7C,GAAmB;IAEnB,OAAO,KAAK,EAAE,IAAqB,EAA6B,EAAE;QAChE,IAAI,MAAyB,CAAC;QAC9B,IAAI,YAAY,EAAE,KAAK,EAAE,CAAC;YACxB,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC3C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,mDAAmD;gBACnD,iFAAiF;gBACjF,MAAM,mBAAmB,GAAG,GAAG,KAAK,aAAa,IAAI,GAAG,KAAK,MAAM,CAAC;gBACpE,MAAM,GAAG;oBACP,EAAE,EAAE,KAAK;oBACT,OAAO,EAAE;wBACP,KAAK,EAAE;4BACL,EAAE,EAAE,KAAK;4BACT,OAAO,EAAE,mBAAmB;gCAC1B,CAAC,CAAC,KAAK,YAAY,KAAK;oCACtB,CAAC,CAAC,KAAK,CAAC,OAAO;oCACf,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;gCACjB,CAAC,CAAC,qBAAqB;yBAC1B;qBACF;iBACF,CAAC;YACJ,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACxB,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;QAErC,OAAO;YACL,MAAM;YACN,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beignet/core",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "type": "module",
5
5
  "description": "Core framework primitives for Beignet",
6
6
  "exports": {
@@ -6,6 +6,28 @@
6
6
  import type { AnyPorts } from "../ports/index.js";
7
7
  import type { HttpRequestLike, HttpResponseLike } from "./types.js";
8
8
 
9
+ /**
10
+ * Per-dependency health detail returned by readiness checks.
11
+ */
12
+ export interface HealthCheckDetail {
13
+ /**
14
+ * Whether this dependency is healthy.
15
+ */
16
+ ok: boolean;
17
+ /**
18
+ * Optional human-readable status. Avoid secrets and raw provider credentials.
19
+ */
20
+ message?: string;
21
+ /**
22
+ * Milliseconds spent running this dependency check.
23
+ */
24
+ durationMs?: number;
25
+ /**
26
+ * Optional safe metadata for operators.
27
+ */
28
+ metadata?: Record<string, unknown>;
29
+ }
30
+
9
31
  /**
10
32
  * Health check result returned by health handlers.
11
33
  */
@@ -17,7 +39,42 @@ export interface HealthCheckResult {
17
39
  /**
18
40
  * Optional per-dependency health details.
19
41
  */
20
- details?: Record<string, { ok: boolean; message?: string }>;
42
+ details?: Record<string, HealthCheckDetail>;
43
+ }
44
+
45
+ /**
46
+ * A named dependency check run by {@link runHealthChecks}.
47
+ */
48
+ export type HealthCheck<Ports> = (
49
+ ports: Ports,
50
+ ) =>
51
+ | Promise<HealthCheckDetail | boolean | undefined>
52
+ | HealthCheckDetail
53
+ | boolean
54
+ | undefined;
55
+
56
+ /**
57
+ * Named dependency checks for an app-owned readiness endpoint.
58
+ */
59
+ export type HealthChecks<Ports> = Record<string, HealthCheck<Ports>>;
60
+
61
+ /**
62
+ * Options for running named dependency checks.
63
+ */
64
+ export interface RunHealthChecksOptions {
65
+ /**
66
+ * Maximum time to wait for each dependency check.
67
+ *
68
+ * Defaults to 2000ms.
69
+ */
70
+ timeoutMs?: number;
71
+ /**
72
+ * Include thrown error messages in dependency details.
73
+ *
74
+ * Defaults to true. Set false in production responses when provider errors
75
+ * may include sensitive details.
76
+ */
77
+ includeErrorDetails?: boolean;
21
78
  }
22
79
 
23
80
  /**
@@ -34,6 +91,10 @@ export interface HealthConfig<Ports> {
34
91
  suggestedPath?: string;
35
92
  /** Custom health check function */
36
93
  check?: (ports: Ports) => Promise<HealthCheckResult>;
94
+ /** Named dependency checks for an app-owned readiness endpoint */
95
+ checks?: HealthChecks<Ports>;
96
+ /** Per-check timeout for named dependency checks */
97
+ timeoutMs?: number;
37
98
  }
38
99
 
39
100
  /**
@@ -41,6 +102,120 @@ export interface HealthConfig<Ports> {
41
102
  */
42
103
  export type AppEnvironment = "development" | "production" | "test";
43
104
 
105
+ const DEFAULT_HEALTH_TIMEOUT_MS = 2000;
106
+
107
+ function errorMessage(error: unknown): string {
108
+ return error instanceof Error ? error.message : String(error);
109
+ }
110
+
111
+ function assertTimeout(timeoutMs: number): void {
112
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
113
+ throw new Error("Health check timeoutMs must be a positive integer.");
114
+ }
115
+ }
116
+
117
+ function normalizeHealthDetail(
118
+ result: HealthCheckDetail | boolean | undefined,
119
+ ): HealthCheckDetail {
120
+ if (typeof result === "boolean") return { ok: result };
121
+ return result ?? { ok: true };
122
+ }
123
+
124
+ function redactHealthDetail(
125
+ detail: HealthCheckDetail,
126
+ includeErrorDetails: boolean,
127
+ ): HealthCheckDetail {
128
+ if (includeErrorDetails || detail.ok || detail.message === undefined) {
129
+ return detail;
130
+ }
131
+
132
+ return {
133
+ ...detail,
134
+ message: "Health check failed",
135
+ };
136
+ }
137
+
138
+ function withTimeout<T>(
139
+ promise: Promise<T>,
140
+ timeoutMs: number,
141
+ name: string,
142
+ ): Promise<T> {
143
+ let timeout: ReturnType<typeof setTimeout> | undefined;
144
+
145
+ const timeoutPromise = new Promise<never>((_, reject) => {
146
+ timeout = setTimeout(() => {
147
+ reject(
148
+ new Error(
149
+ `Health check "${name}" did not complete within ${timeoutMs}ms.`,
150
+ ),
151
+ );
152
+ }, timeoutMs);
153
+ });
154
+
155
+ return Promise.race([promise, timeoutPromise]).finally(() => {
156
+ if (timeout) clearTimeout(timeout);
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Run named dependency health checks in parallel and aggregate the result.
162
+ *
163
+ * This is intended for app-owned readiness endpoints. Checks should be cheap,
164
+ * bounded, non-mutating probes such as `SELECT 1`, Redis `PING`, or provider
165
+ * health endpoints. Do not start workers, drains, migrations, or polling loops
166
+ * from readiness checks.
167
+ */
168
+ export async function runHealthChecks<Ports extends AnyPorts>(
169
+ ports: Ports,
170
+ checks: HealthChecks<Ports>,
171
+ options: RunHealthChecksOptions = {},
172
+ ): Promise<HealthCheckResult> {
173
+ const timeoutMs = options.timeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
174
+ assertTimeout(timeoutMs);
175
+
176
+ const includeErrorDetails = options.includeErrorDetails ?? true;
177
+ const entries = await Promise.all(
178
+ Object.entries(checks).map(async ([name, check]) => {
179
+ const startedAt = Date.now();
180
+
181
+ try {
182
+ const detail = redactHealthDetail(
183
+ normalizeHealthDetail(
184
+ await withTimeout(Promise.resolve(check(ports)), timeoutMs, name),
185
+ ),
186
+ includeErrorDetails,
187
+ );
188
+
189
+ return [
190
+ name,
191
+ {
192
+ ...detail,
193
+ durationMs: Date.now() - startedAt,
194
+ },
195
+ ] as const;
196
+ } catch (error) {
197
+ return [
198
+ name,
199
+ {
200
+ ok: false,
201
+ message: includeErrorDetails
202
+ ? errorMessage(error)
203
+ : "Health check failed",
204
+ durationMs: Date.now() - startedAt,
205
+ },
206
+ ] as const;
207
+ }
208
+ }),
209
+ );
210
+
211
+ const details = Object.fromEntries(entries);
212
+
213
+ return {
214
+ ok: Object.values(details).every((detail) => detail.ok),
215
+ details,
216
+ };
217
+ }
218
+
44
219
  /**
45
220
  * Create a framework-neutral health check handler.
46
221
  *
@@ -76,6 +251,11 @@ export function createHealthHandler<Ports extends AnyPorts>(
76
251
  },
77
252
  };
78
253
  }
254
+ } else if (healthConfig?.checks) {
255
+ result = await runHealthChecks(ports, healthConfig.checks, {
256
+ timeoutMs: healthConfig.timeoutMs,
257
+ includeErrorDetails: env !== "production",
258
+ });
79
259
  } else {
80
260
  result = { ok: true };
81
261
  }