@beignet/core 0.0.20 → 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,17 @@
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
+
9
+ ## 0.0.21
10
+
11
+ ### Patch Changes
12
+
13
+ - Ship TanStack Intent agent skills with Beignet packages and scaffold generated apps with Intent skill-loading guidance.
14
+
3
15
  ## 0.0.20
4
16
 
5
17
  ## 0.0.19
package/README.md CHANGED
@@ -30,6 +30,13 @@ npm install arktype
30
30
 
31
31
  This package requires TypeScript 5.0 or higher for proper type inference.
32
32
 
33
+ ## Agent skills
34
+
35
+ This package ships a TanStack Intent skill for coding agents:
36
+ `@beignet/core#app-architecture`. Load it when adding or fixing Beignet
37
+ schemas, contracts, use cases, app errors, ports, policies, app context,
38
+ providers, workflow primitives, or core subpath imports.
39
+
33
40
  ## Subpaths
34
41
 
35
42
  Install `@beignet/core` once, then import the framework area you need. The
@@ -1262,6 +1269,32 @@ The public adapter contract is `HttpAdapter<NativeRequest, NativeResponse>`.
1262
1269
  Use it when building a runtime package beyond the first-party `@beignet/web`
1263
1270
  and `@beignet/next` adapters.
1264
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
+
1265
1298
  ### Request instrumentation and tracing
1266
1299
 
1267
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.20",
3
+ "version": "0.0.22",
4
4
  "type": "module",
5
5
  "description": "Core framework primitives for Beignet",
6
6
  "exports": {
@@ -150,6 +150,7 @@
150
150
  "!src/**/*.test.ts",
151
151
  "!src/**/*.test.tsx",
152
152
  "!src/**/*.test-d.ts",
153
+ "skills",
153
154
  "README.md",
154
155
  "CHANGELOG.md"
155
156
  ],
@@ -167,7 +168,8 @@
167
168
  "api",
168
169
  "typescript",
169
170
  "standard-schema",
170
- "validation"
171
+ "validation",
172
+ "tanstack-intent"
171
173
  ],
172
174
  "license": "MIT",
173
175
  "repository": {
@@ -0,0 +1,159 @@
1
+ ---
2
+ name: app-architecture
3
+ description: "Build Beignet app feature slices with @beignet/core primitives: schemas, contracts, use cases, app errors, ports, policies, context, providers, jobs, schedules, tasks, outbox, mail, uploads, and tests. Use when adding or fixing Beignet application behavior, dependency boundaries, app-facing ports, authorization, workflow primitives, or core subpath imports."
4
+ ---
5
+
6
+ # Beignet App Architecture
7
+
8
+ Use this skill when working in an app that depends on `@beignet/core`.
9
+ Read the app's `AGENTS.md`, `beignet.config.*`, and closest existing feature
10
+ slice before changing code.
11
+
12
+ ## Source Order
13
+
14
+ 1. Follow local app instructions and configured paths.
15
+ 2. Reuse the nearest feature slice's naming and helper exports.
16
+ 3. Inspect installed `@beignet/core/*` exports and types when an API is unclear.
17
+ 4. Read narrow Beignet docs only for the concept being changed.
18
+
19
+ ## Feature Slice
20
+
21
+ Normal feature code belongs here:
22
+
23
+ ```txt
24
+ features/<feature>/
25
+ contracts.ts
26
+ schemas.ts
27
+ routes.ts
28
+ use-cases/
29
+ ports.ts
30
+ policy.ts
31
+ client/
32
+ components/
33
+ tests/
34
+ factories/
35
+ ```
36
+
37
+ Small features do not need every file. Add folders when the concern exists.
38
+
39
+ ## Core Boundaries
40
+
41
+ - Contracts and schemas must be import-safe for clients and OpenAPI.
42
+ - Use cases own business workflows and depend on `ctx.ports`, not infra.
43
+ - Domain and use-case code must not import React, routes, providers, server
44
+ runtime, or concrete adapters.
45
+ - Infra implements ports and may import provider packages, SDKs, and database
46
+ clients.
47
+ - UI and feature client modules call server behavior through contracts and a
48
+ typed client, not by importing use cases.
49
+
50
+ ## Contracts And Schemas
51
+
52
+ Use `@beignet/core/contracts` and keep shared DTOs in `schemas.ts`.
53
+
54
+ ```ts
55
+ import { defineContractGroup } from "@beignet/core/contracts";
56
+ import { z } from "zod";
57
+ import { errors } from "@/features/shared/errors";
58
+
59
+ export const projectSchema = z.object({
60
+ id: z.string().uuid(),
61
+ name: z.string().min(1),
62
+ });
63
+
64
+ const projects = defineContractGroup()
65
+ .namespace("projects")
66
+ .prefix("/api/projects");
67
+
68
+ export const getProject = projects
69
+ .get("/:id")
70
+ .pathParams(z.object({ id: z.string().uuid() }))
71
+ .errors({ ProjectNotFound: errors.ProjectNotFound })
72
+ .responses({ 200: projectSchema });
73
+ ```
74
+
75
+ If a route-backed use case throws `appError("ProjectNotFound")`, the matching
76
+ contract should declare `ProjectNotFound` with `.errors(...)`.
77
+
78
+ ## Use Cases
79
+
80
+ Use the app-bound `useCase` builder from `lib/use-case.ts`.
81
+
82
+ ```ts
83
+ import { requireUserId } from "@beignet/core/ports";
84
+ import { appError } from "@/features/shared/errors";
85
+ import { projectIdInputSchema, projectSchema } from "@/features/projects/schemas";
86
+ import { useCase } from "@/lib/use-case";
87
+
88
+ export const getProjectUseCase = useCase
89
+ .query("projects.get")
90
+ .input(projectIdInputSchema)
91
+ .output(projectSchema)
92
+ .run(async ({ ctx, input }) => {
93
+ const userId = requireUserId(ctx);
94
+ const project = await ctx.ports.projects.findById(input.id);
95
+ if (!project) throw appError("ProjectNotFound", { details: input });
96
+
97
+ await ctx.gate.authorize("projects.read", project);
98
+ return project;
99
+ });
100
+ ```
101
+
102
+ Use `ctx.ports.uow.transaction(...)` when writes, audit entries, events, jobs,
103
+ or outbox records must commit together.
104
+
105
+ ## Errors And Policies
106
+
107
+ - Generated apps usually place the error catalog in `features/shared/errors.ts`.
108
+ Some apps centralize it elsewhere; reuse the existing catalog.
109
+ - Business authorization belongs in use cases or feature policies, not only
110
+ route metadata.
111
+ - Put a policy in the feature that owns the authorized resource. Cross-feature
112
+ abilities may still live there when they inspect that resource.
113
+
114
+ ## Ports And Providers
115
+
116
+ - Feature-specific ports usually live in `features/<feature>/ports.ts`.
117
+ - App-wide ports live in `ports/`.
118
+ - Default wiring belongs in `infra/app-ports.ts`.
119
+ - `bound` ports are app-owned at boot, such as gates, config, clocks, IDs, or
120
+ no-op reporters.
121
+ - `deferred` ports are supplied by providers at startup, such as auth,
122
+ database, mail, logger, jobs, repositories, idempotency, and rate limits.
123
+
124
+ Do not import provider packages from contracts, use cases, routes, UI, or
125
+ feature client modules.
126
+
127
+ ## Workflows
128
+
129
+ Feature-owned workflow definitions belong under the feature:
130
+
131
+ ```txt
132
+ features/<feature>/jobs/
133
+ features/<feature>/schedules/
134
+ features/<feature>/notifications/
135
+ features/<feature>/tasks/
136
+ features/<feature>/uploads/
137
+ ```
138
+
139
+ Small existing apps may use feature-root `jobs.ts` or `schedules.ts`; follow
140
+ local style unless migrating deliberately. Central registries and service
141
+ contexts live under `server/`.
142
+
143
+ Providers should not start unbounded background loops on server boot. Expose
144
+ cron routes, worker entrypoints, task runners, schedule runners, or outbox
145
+ drains instead.
146
+
147
+ ## Validation
148
+
149
+ After core app-architecture changes, run from the app root:
150
+
151
+ ```bash
152
+ beignet lint
153
+ beignet doctor --strict
154
+ bun run test
155
+ bun run typecheck
156
+ ```
157
+
158
+ Use the app's package manager and scripts. If package-manager shims are not on
159
+ PATH, use local binaries such as `./node_modules/.bin/beignet lint`.
@@ -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
  }