@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 +26 -0
- package/README.md +33 -4
- package/dist/app.d.ts +25 -1
- package/dist/app.js +26 -2
- package/dist/app.js.map +1 -1
- package/dist/health.d.ts +84 -0
- package/dist/health.js +284 -0
- package/dist/health.js.map +1 -0
- package/dist/index.d.ts +25 -3
- package/dist/index.js +62 -42
- package/dist/index.js.map +1 -1
- package/dist/routes/extensions.js +10 -0
- package/dist/routes/extensions.js.map +1 -1
- package/dist/routes/pm.js +24 -37
- package/dist/routes/pm.js.map +1 -1
- package/dist/server.js +33 -2
- package/dist/server.js.map +1 -1
- package/dist/services/mutation-event-watcher.js +2 -1
- package/dist/services/mutation-event-watcher.js.map +1 -1
- package/dist/services/package-catalog.d.ts +24 -0
- package/dist/services/package-catalog.js +22 -2
- package/dist/services/package-catalog.js.map +1 -1
- package/dist/services/pm-runner.d.ts +51 -1
- package/dist/services/pm-runner.js +105 -1
- package/dist/services/pm-runner.js.map +1 -1
- package/manifest.json +3 -5
- package/package.json +20 -18
- package/public/src/i18n/de.json +3 -1
- package/public/src/i18n/en.json +3 -1
- package/public/src/i18n/es.json +3 -1
- package/public/src/i18n/zh.json +3 -1
- package/public/src/views/packages.js +26 -5
- package/public/src/views/packages.js.map +1 -1
- package/public/src/views/packages.ts +33 -5
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
|
-
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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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"}
|
package/dist/health.d.ts
ADDED
|
@@ -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
|
-
/**
|
|
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;
|