@unbrained/pm-web 2026.8.14 → 2026.8.28

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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Fixed
6
+
7
+ - The health endpoint no longer reports ok: true when the database or projects volume is unreachable ([pm-web-u9mh](https://github.com/unbraind/pm-web/blob/main/.agents/pm/issues/pm-web-u9mh.toon))
8
+ - Mount the real /healthz handler in pm-web (Greptile P1 on PR \#105) ([pm-web-iocm](https://github.com/unbraind/pm-web/blob/main/.agents/pm/issues/pm-web-iocm.toon))
9
+
10
+ ## 2026.8.28 - 2026-08-28
11
+
12
+ ### Fixed
13
+
14
+ - The changelog gate stamps an untagged version with the current date, so its verdict flips every midnight with no commit ([pm-web-kcs1](https://github.com/unbraind/pm-web/blob/main/.agents/pm/issues/pm-web-kcs1.toon))
15
+ - Certify pm-web complete tracker reads and refresh the package catalog ([pm-web-crdr](https://github.com/unbraind/pm-web/blob/main/.agents/pm/issues/pm-web-crdr.toon))
16
+ - Docstring corrections: filters, graph-canvas, crypto, board, pm routes, project-watcher, sse, mutation-event-watcher ([pm-web-oehb](https://github.com/unbraind/pm-web/blob/main/.agents/pm/issues/pm-web-oehb.toon))
17
+ - Fix release publish-before-protected-main-push ordering ([pm-web-8l6j](https://github.com/unbraind/pm-web/blob/main/.agents/pm/issues/pm-web-8l6j.toon))
18
+
19
+ ### Security
20
+
21
+ - The identity gate deadlocks the one remediation its own failure message prescribes ([pm-web-1ggj](https://github.com/unbraind/pm-web/blob/main/.agents/pm/issues/pm-web-1ggj.toon))
22
+
23
+ ## 2026.8.17 - 2026-08-17
24
+
25
+ ### Fixed
26
+
27
+ - The package catalog could silently omit a fleet package, and had already drifted from pm-ops capabilities ([pm-web-yqmi](https://github.com/unbraind/pm-web/blob/main/.agents/pm/issues/pm-web-yqmi.toon))
28
+
3
29
  ## 2026.8.14 - 2026-08-14
4
30
 
5
31
  ### Fixed
package/README.md CHANGED
@@ -149,6 +149,19 @@ within each Node process, so high-throughput installations should run multiple
149
149
  pm-web replicas behind a shared PostgreSQL realtime bus. Independent replicas
150
150
  converge through PostgreSQL notifications and the mutation-event watcher.
151
151
 
152
+ Whole-project graph fallback, board, local search, iCalendar, and export reads
153
+ use the SDK's high-level `listAllComplete` operation. pm-web accepts those rows
154
+ only after the shared SDK certificate and supplemental 2026.8.21 source,
155
+ omission, output-receipt, and budget checks all agree. The public `/pm/list-all`
156
+ HTTP compatibility route remains deliberately paginated for interactive clients,
157
+ but invokes canonical `list --all` internally; consumers that need the whole
158
+ workspace must use a complete-read endpoint rather than assembling a page as if
159
+ it were the corpus. The standalone server exact-pins pm CLI/SDK 2026.8.21, and
160
+ the extension manifest refuses older hosts through the same compatibility floor.
161
+ Commands that render their own text or JSON (`web status`, `web stop`, and
162
+ `web doctor`) return the public SDK output-suppression marker, so the host never
163
+ appends a second payload to stdout.
164
+
152
165
  Optional OIDC uses Authorization Code flow with PKCE, provider discovery/JWKS,
153
166
  signed state cookies, and issuer/subject identity mapping. It is disabled when
154
167
  no OIDC variables are present and production startup fails closed on partial or
@@ -186,13 +199,29 @@ MIT
186
199
 
187
200
  ## Release Automation
188
201
 
189
- This package is release-ready for GitHub, npm, and Bun-compatible installs. CI runs type checking, build, production dependency audit, package packing, Bun install verification, and pm-changelog validation. The daily release workflow publishes only when commits exist after the latest release tag and uses pm-changelog to generate CHANGELOG.md and GitHub release notes.
202
+ CI runs type checking, exact Node 22.18.0 and current-Node matrices, real
203
+ PostgreSQL integration tests, the configured coverage gate, complete docstring
204
+ coverage, production dependency audit, package packing, fresh packed `npx` and
205
+ `bunx` install-and-command acceptance, immutable workflow-action checks, and
206
+ pm-changelog validation. The daily release
207
+ workflow publishes at most once when commits exist after the latest release tag
208
+ and uses pm-changelog for both `CHANGELOG.md` and GitHub release notes.
209
+
210
+ The current package is **not** approved for a new release: exact all-source
211
+ 100/100/100/100 coverage remains open in
212
+ [`pm-web-9ulj`](.agents/pm/epics/pm-web-9ulj.toon) and
213
+ [`pm-web-ulgy`](.agents/pm/tasks/pm-web-ulgy.toon), while reachable-history
214
+ privacy authorization is tracked in
215
+ [`pm-web-priv`](.agents/pm/issues/pm-web-priv.toon) and
216
+ [GitHub issue #96](https://github.com/unbraind/pm-web/issues/96). Passing the
217
+ configured gate is evidence for the measured source set, not those independent
218
+ release approvals.
190
219
 
191
220
  ## New data endpoints (kanban board & search)
192
221
 
193
- The pm data API now exposes board and search views, both driven by the
194
- workspace's live `pm contracts` schema (so they reflect the installed pm CLI +
195
- extensions):
222
+ The pm data API exposes board and search views through the certified
223
+ complete-read contract described above. The board also uses the workspace's
224
+ live `pm contracts` statuses for its columns:
196
225
 
197
226
  - `GET /api/projects/:projectId/pm/board` — items grouped into kanban columns by
198
227
  the workspace's runtime statuses (unlisted statuses fall into `(other)`).
package/dist/app.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type Express } from "express";
2
+ import { type HealthProbeDeps } from "./health.ts";
2
3
  /**
3
4
  * Legal pages served as standalone HTML (not part of the SPA bundle).
4
5
  *
@@ -34,10 +35,33 @@ export declare const LEGAL_REDIRECTS: Record<string, string>;
34
35
  * @returns The real, resolved directory path holding the legal HTML files.
35
36
  */
36
37
  export declare function resolveLegalPagesDir(env?: NodeJS.ProcessEnv): string;
38
+ /**
39
+ * Optional dependencies `createApp` can wire into the application.
40
+ *
41
+ * Currently only the {@link HealthProbeDeps} for the real `/healthz` handler is
42
+ * supported; the object is kept as a bag so further production-only wiring
43
+ * (e.g. a logger) can be added later without another signature change.
44
+ */
45
+ export interface CreateAppDeps {
46
+ /**
47
+ * Health-probe dependencies for the real `/healthz` handler. When supplied,
48
+ * `createApp` mounts {@link createHealthHandler}, which probes PostgreSQL and
49
+ * the projects volume and answers 200/503 accordingly. When omitted, the
50
+ * route answers 503 `ok:false` (see the comment at the route) so a
51
+ * misconfigured deployment can never report healthy while its dependencies
52
+ * are down — the exact failure this handler was written to close.
53
+ */
54
+ readonly health?: HealthProbeDeps;
55
+ }
37
56
  /**
38
57
  * Build the Express application with all middleware, static assets, legal
39
58
  * page routes, API routes and the SPA fallback — but WITHOUT touching the
40
59
  * database or binding a port. Splitting this out from server.ts keeps the
41
60
  * HTTP surface unit-testable without a running PostgreSQL instance.
61
+ *
62
+ * @param deps - Optional production wiring. Pass `health` to mount the real
63
+ * probing `/healthz` handler; omit it for tests that only need the HTTP
64
+ * surface (the route then answers 503 `ok:false`, never `ok:true`).
65
+ * @returns The configured Express application, not yet listening on a port.
42
66
  */
43
- export declare function createApp(): Express;
67
+ export declare function createApp(deps?: CreateAppDeps): Express;
package/dist/app.js CHANGED
@@ -3,6 +3,7 @@ import cookieParser from "cookie-parser";
3
3
  import path from "node:path";
4
4
  import { accessSync, constants, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { createHealthHandler } from "./health.js";
6
7
  import { authRouter } from "./routes/auth.js";
7
8
  import { oidcRouter } from "./routes/oidc.js";
8
9
  import { projectsRouter } from "./routes/projects.js";
@@ -95,8 +96,13 @@ export function resolveLegalPagesDir(env = process.env) {
95
96
  * page routes, API routes and the SPA fallback — but WITHOUT touching the
96
97
  * database or binding a port. Splitting this out from server.ts keeps the
97
98
  * HTTP surface unit-testable without a running PostgreSQL instance.
99
+ *
100
+ * @param deps - Optional production wiring. Pass `health` to mount the real
101
+ * probing `/healthz` handler; omit it for tests that only need the HTTP
102
+ * surface (the route then answers 503 `ok:false`, never `ok:true`).
103
+ * @returns The configured Express application, not yet listening on a port.
98
104
  */
99
- export function createApp() {
105
+ export function createApp(deps) {
100
106
  const app = express();
101
107
  const legalPagesDir = resolveLegalPagesDir();
102
108
  app.use(express.json({ limit: "1mb" }));
@@ -128,7 +134,25 @@ export function createApp() {
128
134
  return "unknown";
129
135
  }
130
136
  })();
131
- app.get("/healthz", (_req, res) => res.json({ ok: true, version: PM_WEB_VERSION }));
137
+ if (deps?.health) {
138
+ // Production wiring: probe PostgreSQL and the projects volume before
139
+ // answering. `server.ts` always supplies these dependencies, so the
140
+ // deployed service reports healthy only when its hard dependencies are
141
+ // actually reachable.
142
+ app.get("/healthz", createHealthHandler(deps.health));
143
+ }
144
+ else {
145
+ // No health-probe dependencies were supplied, so this route cannot probe
146
+ // PostgreSQL or the projects volume. Answering `ok:true` here would
147
+ // reproduce the original bug this handler exists to close: pm-web has
148
+ // previously served frozen data for two days while `/healthz` reported
149
+ // healthy. A route that cannot probe must not claim `ok:true`, so the
150
+ // unconfigured default answers 503 with the version only. Production
151
+ // (`server.ts`) always supplies the real dependencies; this branch is
152
+ // reached only by tests that exercise the HTTP surface without a database
153
+ // and by misconfigured deployments, which must fail loud rather than lie.
154
+ app.get("/healthz", (_req, res) => res.status(503).json({ ok: false, version: PM_WEB_VERSION }));
155
+ }
132
156
  const legalPages = new Set(LEGAL_PAGES);
133
157
  Object.entries(LEGAL_REDIRECTS).forEach(([from, to]) => {
134
158
  app.get(from, (_req, res) => {
package/dist/app.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ import type { RequestHandler } from "express";
2
+ /**
3
+ * Minimum structural type a PostgreSQL pool must satisfy for the health probe.
4
+ *
5
+ * The real `pg.Pool` satisfies this, and tests can substitute any object whose
6
+ * `query` accepts a SQL string and returns a promise — so the probe runs
7
+ * through the same pool the service uses in production without forcing tests
8
+ * to construct a real database connection.
9
+ */
10
+ export interface Queryable {
11
+ /** Execute a SQL string and return rows. */
12
+ query(text: string): Promise<{
13
+ rows: unknown[];
14
+ }>;
15
+ }
16
+ /** Status reported for one dependency in the health response. */
17
+ export interface DependencyStatus {
18
+ /** Whether the dependency responded successfully. */
19
+ readonly ok: boolean;
20
+ /** Round-trip latency in milliseconds. */
21
+ readonly latency_ms: number;
22
+ /** Human-readable failure reason; omitted on success. */
23
+ readonly error?: string;
24
+ /**
25
+ * Whether the dependency was configured (env var present). Only meaningful
26
+ * for soft dependencies that may legitimately be absent; hard dependencies
27
+ * are always treated as configured.
28
+ */
29
+ readonly configured?: boolean;
30
+ }
31
+ /** A named soft-dependency probe whose result does not affect the status code. */
32
+ export interface SoftProbe {
33
+ /** Key under `dependencies` in the response. */
34
+ readonly name: string;
35
+ /** Async function returning the dependency status. */
36
+ readonly probe: () => Promise<DependencyStatus>;
37
+ }
38
+ /** Dependencies the health handler needs to probe. */
39
+ export interface HealthProbeDeps {
40
+ /** PostgreSQL pool — probed with `SELECT 1` (hard dependency). */
41
+ readonly pool: Queryable;
42
+ /** Host-mounted projects root — probed for writability (hard dependency). */
43
+ readonly projectsRoot: string;
44
+ /** Package version, resolved once at boot from `package.json`. */
45
+ readonly version: string;
46
+ /** Soft-dependency probes; their results are reported but never cause 503. */
47
+ readonly softProbes?: ReadonlyArray<SoftProbe>;
48
+ /**
49
+ * Timing overrides, in milliseconds.
50
+ *
51
+ * Production never sets these. They exist because the behaviour that matters
52
+ * here -- what happens on the second cooldown expiry of a stall that has not
53
+ * ended -- is otherwise only reachable by a test that waits over half a
54
+ * minute, and a guard nothing executes is how a pool-drain protection stops
55
+ * working without anyone noticing.
56
+ */
57
+ readonly timing?: {
58
+ /** Overrides {@link PROBE_TIMEOUT_MS}. */
59
+ readonly probeTimeoutMs?: number;
60
+ /** Overrides {@link CACHE_TTL_MS}. */
61
+ readonly cacheTtlMs?: number;
62
+ /** Overrides {@link POOL_COOLDOWN_MS}. */
63
+ readonly poolCooldownMs?: number;
64
+ };
65
+ }
66
+ /** Shape of the `/healthz` response body (backward compatible: `ok`, `version`). */
67
+ export interface HealthResult {
68
+ /** `true` only when every hard dependency is reachable. */
69
+ readonly ok: boolean;
70
+ /** Package version from `package.json`. */
71
+ readonly version: string;
72
+ /** Per-dependency breakdown. */
73
+ readonly dependencies: Readonly<Record<string, DependencyStatus>>;
74
+ }
75
+ /**
76
+ * Build the Express `/healthz` request handler.
77
+ *
78
+ * The handler probes every hard dependency in parallel, adds soft-dependency
79
+ * results, caches the outcome for {@link CACHE_TTL_MS} milliseconds, and
80
+ * responds with HTTP 200 (`ok: true`) or 503 (`ok: false`). It never throws:
81
+ * each probe catches its own errors, and the outer `try`/`catch` is a
82
+ * defensive last resort.
83
+ */
84
+ export declare function createHealthHandler(deps: HealthProbeDeps): RequestHandler;
package/dist/health.js ADDED
@@ -0,0 +1,284 @@
1
+ import { readdir, stat, unlink, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ /** Maximum time a single probe may run before it is considered failed. */
5
+ const PROBE_TIMEOUT_MS = 2000;
6
+ /** How long a computed health result is reused before re-probing. */
7
+ const CACHE_TTL_MS = 5000;
8
+ /**
9
+ * How long to skip the PostgreSQL probe after one times out.
10
+ *
11
+ * A timed-out query keeps a pool client until the server answers. Re-probing
12
+ * every cache expiry during a stall would drain the pool; this bounds it.
13
+ */
14
+ const POOL_COOLDOWN_MS = 30_000;
15
+ /** How old a stray `.healthz-*.tmp` file must be before a sweep removes it. */
16
+ const STRAY_PROBE_MAX_AGE_MS = 60_000;
17
+ /** Extract a string message from an unknown rejection value. */
18
+ function errorMessage(error) {
19
+ // `/healthz` is unauthenticated, so the response must never carry a raw
20
+ // message: a pg failure names the host, port, database and user, and a
21
+ // filesystem failure names the projects-root path. Both are reconnaissance
22
+ // for an anonymous caller. Map to a stable category and keep the detail in
23
+ // the server log, where an operator can still read it.
24
+ const code = typeof error === "object" && error !== null && "code" in error
25
+ ? String(error.code)
26
+ : "";
27
+ const message = error instanceof Error ? error.message : String(error);
28
+ console.error("[healthz] probe failed", { code, message });
29
+ if (message === "probe timed out")
30
+ return "timed out";
31
+ if (code === "ENOENT")
32
+ return "not found";
33
+ if (code === "EACCES" || code === "EPERM" || code === "EROFS")
34
+ return "permission denied";
35
+ if (code === "ENOSPC")
36
+ return "no space left on device";
37
+ if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EHOSTUNREACH") {
38
+ return "unreachable";
39
+ }
40
+ return "unavailable";
41
+ }
42
+ /**
43
+ * Race a promise against a timeout, rejecting with `"probe timed out"` if the
44
+ * timeout fires first.
45
+ *
46
+ * The timer is cleared in both branches so a resolved/rejected probe does not
47
+ * leak a dangling timer, and a timed-out probe does not keep the event loop
48
+ * alive for the remaining lifetime of the underlying promise.
49
+ */
50
+ function withTimeout(promise, ms) {
51
+ return new Promise((resolve, reject) => {
52
+ const timer = setTimeout(() => reject(new Error("probe timed out")), ms);
53
+ promise.then((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error); });
54
+ });
55
+ }
56
+ /**
57
+ * Probe PostgreSQL by running `SELECT 1` through the provided queryable.
58
+ *
59
+ * Uses the service's real pool (not a fresh connection) so pool exhaustion is
60
+ * itself a failure mode the probe catches. The probe is bounded by
61
+ * {@link PROBE_TIMEOUT_MS}.
62
+ */
63
+ function probePostgres(pool, timeoutMs) {
64
+ const start = Date.now();
65
+ // The raw query is returned alongside the status because the two settle at
66
+ // different times: the status settles when the timeout fires, the query only
67
+ // when PostgreSQL answers, and it is the query that holds a pool client.
68
+ // Anything deciding whether to start another probe has to wait on the query.
69
+ const query = pool.query("SELECT 1");
70
+ const status = withTimeout(query, timeoutMs).then(() => ({ ok: true, latency_ms: Date.now() - start }), (error) => ({ ok: false, latency_ms: Date.now() - start, error: errorMessage(error) }));
71
+ return { query, status };
72
+ }
73
+ /**
74
+ * Probe the projects root by writing and immediately removing a temp file
75
+ * inside it.
76
+ *
77
+ * A missing directory, a read-only host mount, or a full disk all surface as
78
+ * a write or unlink failure. The temp file is cleaned up in every path: on
79
+ * success it is unlinked by the probe itself, and on failure a best-effort
80
+ * cleanup removes it if the write succeeded but the unlink (or timeout) did
81
+ * not.
82
+ */
83
+ async function probeProjectsRoot(root) {
84
+ const start = Date.now();
85
+ // A pid-and-millisecond name collides when two uncached probes start in the
86
+ // same millisecond: whichever unlinks first makes the other report ENOENT and
87
+ // return a spurious 503 for a perfectly healthy volume.
88
+ const probeFile = path.join(root, `.healthz-${randomUUID()}.tmp`);
89
+ // Held so cleanup can be attached to the attempt's own settlement rather than
90
+ // racing it. A timeout does not cancel the underlying write: unlinking
91
+ // immediately can therefore run BEFORE a slow write lands, leaving the file
92
+ // behind permanently - the opposite of what the cleanup is for.
93
+ const attempt = (async () => {
94
+ await writeFile(probeFile, "");
95
+ await unlink(probeFile);
96
+ })();
97
+ try {
98
+ await withTimeout(attempt, PROBE_TIMEOUT_MS);
99
+ return { ok: true, latency_ms: Date.now() - start };
100
+ }
101
+ catch (error) {
102
+ // NOT awaited, and bounded. A stalled mount is the exact condition that
103
+ // reaches this path, and an unbounded `await unlink` here would never
104
+ // settle: probeProjectsRoot would never resolve, the Promise.all in
105
+ // compute() would never resolve, `inFlight` would never clear, and every
106
+ // subsequent /healthz request would await the same pending promise forever
107
+ // - serving nothing at all instead of 503.
108
+ void attempt.catch(() => undefined).finally(() => {
109
+ void withTimeout(unlink(probeFile), PROBE_TIMEOUT_MS).catch(() => undefined);
110
+ });
111
+ return { ok: false, latency_ms: Date.now() - start, error: errorMessage(error) };
112
+ }
113
+ }
114
+ /**
115
+ * Remove `.healthz-*.tmp` files older than {@link STRAY_PROBE_MAX_AGE_MS}.
116
+ *
117
+ * A probe whose write lands after its own cleanup has already run leaves a
118
+ * stray file that nothing else will remove. Sweeping on each healthy probe
119
+ * bounds how long such a file can persist, without ever failing the probe:
120
+ * a sweep error is not a health signal and is deliberately swallowed.
121
+ *
122
+ * @param root - The projects root to sweep.
123
+ */
124
+ async function sweepStrayProbeFiles(root) {
125
+ try {
126
+ const now = Date.now();
127
+ for (const name of await readdir(root)) {
128
+ if (!name.startsWith(".healthz-") || !name.endsWith(".tmp"))
129
+ continue;
130
+ const full = path.join(root, name);
131
+ const info = await stat(full);
132
+ if (now - info.mtimeMs > STRAY_PROBE_MAX_AGE_MS)
133
+ await unlink(full);
134
+ }
135
+ }
136
+ catch {
137
+ // Sweeping is housekeeping, not a dependency check.
138
+ }
139
+ }
140
+ /**
141
+ * Run a soft-dependency probe, catching any synchronous throw, rejection, or
142
+ * timeout so a broken soft probe can never throw into the health handler.
143
+ */
144
+ async function runSoftProbe(probe) {
145
+ try {
146
+ const status = await withTimeout(probe.probe(), PROBE_TIMEOUT_MS);
147
+ return [probe.name, status];
148
+ }
149
+ catch (error) {
150
+ return [probe.name, { ok: false, latency_ms: 0, error: errorMessage(error) }];
151
+ }
152
+ }
153
+ /**
154
+ * Build the Express `/healthz` request handler.
155
+ *
156
+ * The handler probes every hard dependency in parallel, adds soft-dependency
157
+ * results, caches the outcome for {@link CACHE_TTL_MS} milliseconds, and
158
+ * responds with HTTP 200 (`ok: true`) or 503 (`ok: false`). It never throws:
159
+ * each probe catches its own errors, and the outer `try`/`catch` is a
160
+ * defensive last resort.
161
+ */
162
+ export function createHealthHandler(deps) {
163
+ let cached = null;
164
+ // Concurrent cache misses must share ONE computation. `withTimeout` rejects
165
+ // its wrapper but cannot cancel the underlying `pool.query`, so without this
166
+ // every request during a hung database starts another query that keeps
167
+ // running after the endpoint has already answered 503 - consuming pool
168
+ // capacity and starving the ordinary API. Single-flight bounds the damage to
169
+ // one in-flight probe no matter how often the endpoint is polled.
170
+ let inFlight = null;
171
+ // Epoch ms of the last PostgreSQL probe timeout, or 0 if none has timed out.
172
+ let lastPostgresTimeoutAt = 0;
173
+ // A timed-out probe's query is still outstanding: `withTimeout` rejects its
174
+ // wrapper, it cannot cancel `pool.query`. Elapsed time alone is therefore the
175
+ // wrong condition to start another one -- during a prolonged stall the
176
+ // cooldown expires on schedule while the previous client is still checked
177
+ // out, and the pool drains one client per cooldown. This holds the query
178
+ // itself, so at most one probe is ever outstanding however long the database
179
+ // takes to answer.
180
+ let outstandingPostgresProbe;
181
+ const probeTimeoutMs = deps.timing?.probeTimeoutMs ?? PROBE_TIMEOUT_MS;
182
+ const cacheTtlMs = deps.timing?.cacheTtlMs ?? CACHE_TTL_MS;
183
+ const poolCooldownMs = deps.timing?.poolCooldownMs ?? POOL_COOLDOWN_MS;
184
+ /**
185
+ * Probe every dependency once and cache the outcome.
186
+ *
187
+ * Separated from the handler so concurrent cache misses can await a single
188
+ * shared promise rather than each starting their own probe round.
189
+ *
190
+ * @returns The freshly computed health result, already written to the cache.
191
+ */
192
+ async function compute() {
193
+ let result;
194
+ try {
195
+ // `withTimeout` rejects its wrapper but cannot cancel `pool.query`, so a
196
+ // timed-out probe leaves a client checked out until PostgreSQL answers.
197
+ // Without a cooldown, every cache expiry starts another one and a stalled
198
+ // database drains the shared pool out from under the ordinary API. After
199
+ // a timeout the probe is not retried for POOL_COOLDOWN_MS; the endpoint
200
+ // keeps answering 503 from the reported status in the meantime.
201
+ /**
202
+ * Start one PostgreSQL probe and hold its query until PostgreSQL answers.
203
+ *
204
+ * @returns The probe's status.
205
+ */
206
+ const startPostgresProbe = () => {
207
+ const { query, status } = probePostgres(deps.pool, probeTimeoutMs);
208
+ // Cleared when the QUERY settles, not when the status does. A timed-out
209
+ // probe reports in milliseconds while its client stays checked out
210
+ // until PostgreSQL answers, so clearing on the status would let the
211
+ // next cooldown expiry check out another one on top of it.
212
+ outstandingPostgresProbe = query;
213
+ const release = () => {
214
+ if (outstandingPostgresProbe === query)
215
+ outstandingPostgresProbe = undefined;
216
+ };
217
+ void query.then(release, release);
218
+ return status;
219
+ };
220
+ const cooling = Date.now() - lastPostgresTimeoutAt < poolCooldownMs;
221
+ const outstanding = outstandingPostgresProbe !== undefined;
222
+ const postgresProbe = cooling || outstanding
223
+ ? Promise.resolve({
224
+ ok: false,
225
+ latency_ms: 0,
226
+ error: outstanding ? "previous probe still outstanding" : "probe cooling down after timeout",
227
+ })
228
+ : startPostgresProbe().then((status) => {
229
+ // "timed out" is errorMessage()'s stable category, not the raw
230
+ // message: /healthz is unauthenticated, so the raw text is
231
+ // deliberately never surfaced. Matching the category is what makes
232
+ // this survive a change to the underlying error string.
233
+ if (!status.ok && status.error === "timed out") {
234
+ lastPostgresTimeoutAt = Date.now();
235
+ }
236
+ return status;
237
+ });
238
+ const [postgres, projectsRootStatus, ...softResults] = await Promise.all([
239
+ postgresProbe,
240
+ probeProjectsRoot(deps.projectsRoot),
241
+ ...(deps.softProbes?.map(runSoftProbe) ?? []),
242
+ ]);
243
+ const dependencies = {
244
+ postgres,
245
+ projects_root: projectsRootStatus,
246
+ };
247
+ for (const [name, status] of softResults) {
248
+ dependencies[name] = status;
249
+ }
250
+ if (projectsRootStatus.ok)
251
+ void sweepStrayProbeFiles(deps.projectsRoot);
252
+ result = {
253
+ ok: postgres.ok && projectsRootStatus.ok,
254
+ version: deps.version,
255
+ dependencies,
256
+ };
257
+ }
258
+ catch {
259
+ // Every probe catches its own errors; this is a defensive last resort
260
+ // so the handler can never throw an unhandled error into Express.
261
+ result = {
262
+ ok: false,
263
+ version: deps.version,
264
+ dependencies: {
265
+ postgres: { ok: false, latency_ms: 0, error: "unexpected handler error" },
266
+ projects_root: { ok: false, latency_ms: 0, error: "unexpected handler error" },
267
+ },
268
+ };
269
+ }
270
+ cached = { result, at: Date.now() };
271
+ return result;
272
+ }
273
+ const handler = async (_req, res) => {
274
+ if (cached && Date.now() - cached.at < cacheTtlMs) {
275
+ res.status(cached.result.ok ? 200 : 503).json(cached.result);
276
+ return;
277
+ }
278
+ inFlight ??= compute().finally(() => { inFlight = null; });
279
+ const result = await inFlight;
280
+ res.status(result.ok ? 200 : 503).json(result);
281
+ };
282
+ return handler;
283
+ }
284
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.js","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ExtensionApi } from "@unbrained/pm-cli/sdk";
1
+ import { type ExtensionApi, type SuppressedHostOutput } from "@unbrained/pm-cli/sdk";
2
2
  /** Resolve the port from flag → PORT env → default 4000. */
3
3
  export declare function resolvePort(options: Record<string, unknown>, env?: NodeJS.ProcessEnv): string;
4
4
  /**
@@ -19,21 +19,43 @@ export declare function pidfilePath(port: string | number, env?: NodeJS.ProcessE
19
19
  * @returns True when the version is at least 22.18.0.
20
20
  */
21
21
  export declare function nodeVersionMeetsRequirement(version?: string): boolean;
22
- /** Shape a /healthz probe outcome into a stable status result object. */
22
+ /**
23
+ * Shape a /healthz probe outcome into a stable status result object.
24
+ *
25
+ * Reachability and readiness are separate answers. A server whose dependencies
26
+ * are down answers 503 while being perfectly reachable, and reporting that as
27
+ * DOWN sends an operator to look for a process that is running -- so the two
28
+ * are reported separately, with "degraded" naming the state in between.
29
+ */
23
30
  export declare function shapeStatusResult(input: {
24
31
  port: string | number;
25
32
  reachable: boolean;
33
+ healthy?: boolean;
26
34
  body?: unknown;
27
35
  error?: string;
28
36
  }): {
29
- status: "up" | "down";
37
+ status: "up" | "degraded" | "down";
30
38
  port: number;
31
39
  reachable: boolean;
40
+ healthy: boolean;
32
41
  url: string;
33
42
  version: string | null;
34
43
  healthz: unknown;
35
44
  error?: string;
36
45
  };
46
+ /**
47
+ * Write a command-owned JSON or human-readable payload exactly once.
48
+ *
49
+ * The public SDK marker retains the structured result for hooks and embedded
50
+ * hosts while preventing the CLI presentation layer from appending a second
51
+ * serialization to stdout.
52
+ *
53
+ * @param json - Whether to render the structured JSON representation.
54
+ * @param result - Structured command result retained for host integrations.
55
+ * @param humanLines - Lines rendered for an interactive non-JSON invocation.
56
+ * @returns The host-output suppression marker carrying the structured result.
57
+ */
58
+ export declare function emitOwnedOutput<TResult>(json: boolean, result: TResult, humanLines: readonly string[]): SuppressedHostOutput<TResult>;
37
59
  declare const _default: {
38
60
  name: string;
39
61
  version: string;