iit-mcp-hudu 0.1.0 → 0.2.1
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/.env.example +44 -0
- package/README.md +165 -23
- package/dist/config.d.ts +9 -0
- package/dist/config.js +62 -0
- package/dist/credential.d.ts +67 -0
- package/dist/credential.js +101 -0
- package/dist/doctor.d.ts +64 -0
- package/dist/doctor.js +79 -0
- package/dist/http.d.ts +17 -2
- package/dist/http.js +23 -3
- package/dist/main.js +36 -0
- package/dist/tools/dedicated.d.ts +16 -3
- package/dist/tools/dedicated.js +19 -4
- package/dist/tools/dispatch.js +8 -4
- package/dist/tools/fetch-many.d.ts +42 -0
- package/dist/tools/fetch-many.js +63 -0
- package/dist/tools/lists.js +4 -4
- package/dist/tools/reads.js +2 -2
- package/dist/tools/register.d.ts +3 -2
- package/dist/tools/register.js +5 -2
- package/dist/tools/results.d.ts +75 -7
- package/dist/tools/results.js +177 -17
- package/dist/tools/schemas.js +5 -5
- package/dist/tools/search-policy.d.ts +41 -0
- package/dist/tools/search-policy.js +67 -7
- package/dist/tools/search.js +1 -1
- package/dist/tools/server.js +106 -3
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +1 -1
- package/package.json +5 -2
package/.env.example
CHANGED
|
@@ -17,6 +17,32 @@ MCP_TRANSPORT=stdio
|
|
|
17
17
|
# WITHOUT its own key runs as this key, so set it only when that is intended. It is only ever
|
|
18
18
|
# sent to HUDU_BASE_URL, never to a caller-named origin. Left commented so a copied example
|
|
19
19
|
# does not set an empty value.
|
|
20
|
+
#
|
|
21
|
+
# HOW TO LOAD THIS FILE -- read before the first live call.
|
|
22
|
+
# ---------------------------------------------------------------------------------------------
|
|
23
|
+
# Source it with a shell, which is what consumes the single quotes the values below are written in:
|
|
24
|
+
#
|
|
25
|
+
# set -a; . ./.env; set +a
|
|
26
|
+
#
|
|
27
|
+
# Do NOT use a quote-preserving dotenv parser (a plain KEY=VALUE split that keeps the quotes) on
|
|
28
|
+
# this file. It hands `'...'` -- quotes included -- to the server, and Hudu answers 401 for a
|
|
29
|
+
# credential that was never wrong. The trap is sharpest when the secret contains `$`:
|
|
30
|
+
# HUDU_API_KEY='a$b' is `a$b` when sourced and `'a$b'` when naively parsed, and both look correct
|
|
31
|
+
# in the file. A wrong credential is a real authentication failure against your Hudu account, and
|
|
32
|
+
# repeated ones risk an account lock.
|
|
33
|
+
#
|
|
34
|
+
# Then run the zero-wire preflight before any live session:
|
|
35
|
+
#
|
|
36
|
+
# npx -y iit-mcp-hudu doctor # or: node dist/main.js doctor
|
|
37
|
+
#
|
|
38
|
+
# It prints length + two-character prefix + shapeOk per credential field (never a value) and dials
|
|
39
|
+
# nothing, and exits non-zero when the credential would be refused. CONTRACT: a green doctor makes
|
|
40
|
+
# any later 401 vendor-side (rotated, revoked or role-restricted key) -- stop and check the vendor
|
|
41
|
+
# rather than re-dialing.
|
|
42
|
+
#
|
|
43
|
+
# The server enforces the same shape anyway: preserved surrounding quotes, an embedded quote, a
|
|
44
|
+
# carriage return, a newline, edge whitespace or a NUL byte in the value fails boot for
|
|
45
|
+
# HUDU_API_KEY, and is refused with CREDENTIAL_MALFORMED for a per-request X-Hudu-Api-Key.
|
|
20
46
|
# HUDU_API_KEY=
|
|
21
47
|
|
|
22
48
|
# http only, optional. Hosts a caller may name in X-Hudu-Base-Url: comma-separated exact hostnames or
|
|
@@ -62,6 +88,24 @@ LOG_LEVEL=info
|
|
|
62
88
|
# articles and caps assets at ~4,000.
|
|
63
89
|
HUDU_SEARCH_MAX_INDEX_PAGES=40
|
|
64
90
|
|
|
91
|
+
# The read-through cache the SDK clients use (SDK 0.12.0). on (the default) selects the SDK's
|
|
92
|
+
# per-entity TTL preset (1 h structural / 5 min mutable / 1 min base): repeated reads of the
|
|
93
|
+
# same record are served from the cache until the TTL expires. off disables it: every read
|
|
94
|
+
# re-dials Hudu.
|
|
95
|
+
# HUDU_CACHE_PRESET=on
|
|
96
|
+
|
|
97
|
+
# The 429 cooldown gate (SDK 0.12.0). on (the default) arms a local deadline after a vendor
|
|
98
|
+
# 429 whose retries are spent (the profile's 1 retry: the first such call dials twice): calls
|
|
99
|
+
# the gate refuses are answered locally with notSent: true, never re-dialed. off
|
|
100
|
+
# retries a 429 per the agent profile's retry budget instead of arming a local deadline.
|
|
101
|
+
# HUDU_COOLDOWN=on
|
|
102
|
+
|
|
103
|
+
# The whole-call deadline in milliseconds (SDK 0.12.0). Must be a positive integer. Defaults
|
|
104
|
+
# to 30000 and is always sent to the SDK explicitly, so it stays above the agent profile's
|
|
105
|
+
# 20 s deadline regardless of preset: the default keeps the tolerance a cold tier:'index'
|
|
106
|
+
# body-index walk had before the 0.12.0 adoption.
|
|
107
|
+
# HUDU_TIMEOUT_MS=30000
|
|
108
|
+
|
|
65
109
|
# The resources hudu_search and hudu_resolve_any may offer and query. Unset means the SDK's full
|
|
66
110
|
# searchable set (articles, asset_passwords, assets, companies, groups, password_folders, users,
|
|
67
111
|
# websites). Set it to the subset THIS deployment's Hudu API key can read, so a tool neither
|
package/README.md
CHANGED
|
@@ -4,9 +4,10 @@ MCP server for Hudu IT documentation, built on the [`node-hudu`](https://www.npm
|
|
|
4
4
|
|
|
5
5
|
## Tool surface
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
core reads, and four bounded list reads. Setting
|
|
9
|
-
`hudu_delete` (
|
|
7
|
+
20 tools by default: `hudu_list_operations`, `hudu_describe_operation`, `hudu_read`,
|
|
8
|
+
`hudu_fetch_many`, twelve core reads, and four bounded list reads. Setting
|
|
9
|
+
`HUDU_READ_ONLY=false` adds `hudu_write` and `hudu_delete` (22 tools). Each dispatcher uses a
|
|
10
|
+
separate SDK client in its matching mode,
|
|
10
11
|
with names, annotations and operation enums from the SDK's curated metadata. SDK enforcement
|
|
11
12
|
rejects cross-effect calls before requests. The old `hudu_invoke`, `hudu_catalog` and
|
|
12
13
|
`hudu_describe` names are removed; consumers must resync their tool catalog.
|
|
@@ -24,6 +25,18 @@ request; `asset_layouts` is not searchable (the SDK declares no vendor text filt
|
|
|
24
25
|
`hudu_list_asset_layouts` follows the same reachability as the existing ungated
|
|
25
26
|
`hudu_get_asset_layout` tool.
|
|
26
27
|
|
|
28
|
+
These tools run on node-hudu 0.13.1; the `SDK behaviour (0.12.0)` section below covers the behaviours that shape them: redirect refusal, the closed input contract, the error envelope, and the client opt-ins. Numeric ids must be positive integers on every tool; anything else is refused before a request is made.
|
|
29
|
+
|
|
30
|
+
`hudu_fetch_many` (node-hudu 0.12.0) fetches up to 20 single records across mixed resources in
|
|
31
|
+
one call: each item is exactly `{ resource, id, fields? }` — one of the 22 bare-id resources, a
|
|
32
|
+
numeric vendor id, and an optional client-side field projection. A shape violation is refused at
|
|
33
|
+
the schema layer before any request, and failure is per-item: a miss or a vendor fault lands on
|
|
34
|
+
its own row as a typed error while the rest of the batch is served. The deployment's secret-read
|
|
35
|
+
policy applies to the batch's items exactly as to a single read, so a credential item is
|
|
36
|
+
refused pre-request under the default `HUDU_SECRET_READS=deny`; `HUDU_READ_ONLY` keeps the tool
|
|
37
|
+
— it is a read. The same batch stays reachable through `hudu_read` with operation
|
|
38
|
+
`operations.fetchMany`, gated the same way.
|
|
39
|
+
|
|
27
40
|
## Transports
|
|
28
41
|
|
|
29
42
|
| Transport | For | Caller auth | Hudu credential |
|
|
@@ -36,21 +49,12 @@ request; `asset_layouts` is not searchable (the SDK declares no vendor text filt
|
|
|
36
49
|
Needs Node 24+, a Hudu API key and your Hudu origin. `stdio` is the default transport, so no
|
|
37
50
|
`MCP_TRANSPORT` is needed. Mutation tools stay off unless you set `HUDU_READ_ONLY=false`.
|
|
38
51
|
|
|
39
|
-
**From
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
git clone https://github.com/msoukhomlinov/iit-mcp-hudu.git
|
|
43
|
-
cd iit-mcp-hudu
|
|
44
|
-
npm install
|
|
45
|
-
npm run build
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Then register `dist/main.js` with your client, e.g. Claude Code:
|
|
52
|
+
**From npm** (recommended):
|
|
49
53
|
|
|
50
54
|
```
|
|
51
55
|
claude mcp add hudu \
|
|
52
56
|
--env HUDU_API_KEY=your-key --env HUDU_BASE_URL=https://your.hudu.host \
|
|
53
|
-
--
|
|
57
|
+
-- npx -y iit-mcp-hudu
|
|
54
58
|
```
|
|
55
59
|
|
|
56
60
|
or in `.mcp.json` / Claude Desktop config:
|
|
@@ -59,23 +63,70 @@ or in `.mcp.json` / Claude Desktop config:
|
|
|
59
63
|
{
|
|
60
64
|
"mcpServers": {
|
|
61
65
|
"hudu": {
|
|
62
|
-
"command": "
|
|
63
|
-
"args": ["
|
|
66
|
+
"command": "npx",
|
|
67
|
+
"args": ["-y", "iit-mcp-hudu"],
|
|
64
68
|
"env": { "HUDU_API_KEY": "your-key", "HUDU_BASE_URL": "https://your.hudu.host" }
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
```
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
`npx` fetches the package on first run and caches it; use `iit-mcp-hudu@<version>` to pin one.
|
|
75
|
+
|
|
76
|
+
**From a clone** (for development): see [Development](#development). Point your client at
|
|
77
|
+
`node /absolute/path/to/iit-mcp-hudu/dist/main.js` after `npm run build`.
|
|
78
|
+
|
|
79
|
+
## Loading credential values (read this before a live session)
|
|
80
|
+
|
|
81
|
+
A Hudu credential whose bytes reach this server mangled costs a real authentication failure against
|
|
82
|
+
your Hudu account, and repeated ones risk an account lock. Two rules keep that from happening.
|
|
83
|
+
|
|
84
|
+
**Load an env file the way the file documents — shell sourcing, not a generic dotenv parse.**
|
|
71
85
|
|
|
72
86
|
```
|
|
73
|
-
|
|
74
|
-
--env HUDU_API_KEY=your-key --env HUDU_BASE_URL=https://your.hudu.host \
|
|
75
|
-
-- npx -y iit-mcp-hudu
|
|
87
|
+
set -a; . ./.env; set +a # the .env.example recipe, for a file of single-quoted values
|
|
76
88
|
```
|
|
77
89
|
|
|
78
|
-
|
|
90
|
+
`.env.example` writes its example values in single quotes, which is the normal `.env` style. A
|
|
91
|
+
**shell** consumes those quotes when the file is sourced, so the value the process sees is exactly
|
|
92
|
+
the secret. A **quote-preserving dotenv parser** (`dotenv`-style `KEY=VALUE` splitting that keeps
|
|
93
|
+
the quotes) does not: it hands `'…'` to the server, quotes included, and the vendor rejects it.
|
|
94
|
+
The trap is sharpest when the secret itself contains `$` — `HUDU_API_KEY='a$b'` becomes `a$b` when
|
|
95
|
+
sourced and `'a$b'` (with quotes) when naively parsed, and both look correct in the file.
|
|
96
|
+
|
|
97
|
+
The client-config and `--env` paths shown under *Local use* pass values verbatim and never parse a
|
|
98
|
+
file, so they are already safe — this rule is for anyone loading a `.env`.
|
|
99
|
+
|
|
100
|
+
**Run `doctor` before the first live call.** It answers "did my credential load intact?" from the
|
|
101
|
+
environment alone, with zero Hudu requests:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
npx -y iit-mcp-hudu doctor # or: node dist/main.js doctor
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
It prints, per credential field, the value's `length`, its two-character `prefix2` and a `shapeOk`
|
|
108
|
+
flag with the anomaly names when it is not; plus the origin that will actually be dialed. It exits
|
|
109
|
+
non-zero when the credential would be refused, so a runbook step can gate on it:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
node dist/main.js doctor && <start the live session>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**The contract: a green `doctor` makes any later 401 vendor-side.** With `doctor` clean, a 401 means
|
|
116
|
+
Hudu refused a well-shaped credential — a rotated, revoked, or role-restricted key — and the fix is
|
|
117
|
+
at the vendor, not in the load recipe. Do not re-dial to test it: a 401 at the auth gate is the
|
|
118
|
+
signal to stop and check, because repeated failures risk locking the account. Conversely, a 401
|
|
119
|
+
whose envelope names a shape anomaly (`credentialShape: "anomalous"`) points back at the load.
|
|
120
|
+
|
|
121
|
+
The server enforces the shape itself, so a bad load cannot reach the wire even if `doctor` is
|
|
122
|
+
skipped: a malformed `HUDU_API_KEY` fails boot; a malformed `X-Hudu-Api-Key` header is refused
|
|
123
|
+
before any client is built, with the typed `CREDENTIAL_MALFORMED` envelope (value-free, and
|
|
124
|
+
`retryable: false`). Neither path dials.
|
|
125
|
+
|
|
126
|
+
The server inspects **shape only** — length, a two-character prefix, and anomaly names. A credential
|
|
127
|
+
value is never logged, never echoed in an error, and never returned by `/doctor`. The anomalies that
|
|
128
|
+
fail a value are: preserved surrounding quotes (single or double), an embedded quote, a carriage
|
|
129
|
+
return, a newline, edge whitespace, and a NUL byte.
|
|
79
130
|
|
|
80
131
|
## Configuration
|
|
81
132
|
|
|
@@ -143,7 +194,14 @@ credential. `asset_passwords.get` returns `password` and `otp_secret` as an ordi
|
|
|
143
194
|
curated tools `hudu_find_asset_passwords_by_slug` and `hudu_get_password_folder` reach the same
|
|
144
195
|
records. It is `deny` by default and refuses every operation on the `asset_passwords` and
|
|
145
196
|
`password_folders` resources — through `hudu_read` and through those two tools alike, before any
|
|
146
|
-
request is issued — so a deployment cannot hand a secret to a model by omission.
|
|
197
|
+
request is issued — so a deployment cannot hand a secret to a model by omission. The batch read
|
|
198
|
+
`operations.fetchMany` (reachable through `hudu_read`) is refused the same way, per item: its
|
|
199
|
+
operation key is not itself a credential read, so each item's resource is checked before any
|
|
200
|
+
request, and a mixed batch is refused whole rather than partially dialed. The
|
|
201
|
+
cross-resource resolve `operations.resolveAny` (also reachable through `hudu_read`) is gated the
|
|
202
|
+
same way: under deny an explicitly named credential resource is refused, and an omitted resource
|
|
203
|
+
list resolves to the permitted set, which excludes the credential resources from the fan-out. It
|
|
204
|
+
is scoped to the
|
|
147
205
|
whole resource rather than to the `expand` flag, because `asset_passwords.get` returns the secret
|
|
148
206
|
without `expand`. It is independent of `HUDU_WRITE_POLICY`: `all` does not imply open secrets, and
|
|
149
207
|
`allow` does not open writes. `hudu_search` is unaffected — it already redacts hits on those two
|
|
@@ -176,17 +234,97 @@ background, so a caller's later search is body-aware without the first one await
|
|
|
176
234
|
walk (some clients enforce a 60s call timeout). Under `stdio` the one long-lived index stays warm and the
|
|
177
235
|
default is `"index"`. Pass `tier:"index"` explicitly for a complete first answer.
|
|
178
236
|
|
|
237
|
+
Three 0.12.0 preset knobs are env-tunable, each defaulting to the adopted behaviour.
|
|
238
|
+
`HUDU_CACHE_PRESET` (`on` | `off`) toggles the read-through cache: the default `on` selects
|
|
239
|
+
the SDK's `recommended` per-entity TTL preset, and `off` disables it, so repeated reads
|
|
240
|
+
re-dial Hudu instead of being served from the cache.
|
|
241
|
+
`HUDU_COOLDOWN` (`on` | `off`) is the 429 cooldown gate: `off` means a 429 retries per the
|
|
242
|
+
agent profile instead of arming a local deadline whose refusals answer with `notSent: true`.
|
|
243
|
+
`HUDU_TIMEOUT_MS` (positive integer milliseconds) is the whole-call deadline, default `30000`;
|
|
244
|
+
it is always sent to the SDK explicitly, so it stays above the profile's 20 s.
|
|
245
|
+
`blockPrivateHosts` deliberately gets no override: it is the literal-IP SSRF floor for
|
|
246
|
+
caller-named origins — a security property, not a deployment preference.
|
|
247
|
+
|
|
248
|
+
## SDK behaviour (0.12.0)
|
|
249
|
+
|
|
250
|
+
Three SDK behaviours changed in the 0.12.0 line and shape a deployment:
|
|
251
|
+
|
|
252
|
+
- **Redirects are refused, not followed.** Every Hudu request is sent with `redirect: 'manual'`;
|
|
253
|
+
a 3xx answer (any except 304) is a `REDIRECT_BLOCKED` error carrying the `location`, and the SDK
|
|
254
|
+
never follows it. If a reverse proxy in front of the Hudu origin answers 301/302 (scheme upgrade,
|
|
255
|
+
trailing slash, host rename), dial the final origin instead — the fix is topology, not config.
|
|
256
|
+
- **Reads have a closed input contract.** `hudu_read` now refuses unknown input fields at the
|
|
257
|
+
schema layer with a typed `CONFIG_ERROR` naming the accepted fields, before any request;
|
|
258
|
+
`hudu_describe_operation` serves the same contract as `input_contract`. The batch read
|
|
259
|
+
`operations.fetchMany` (up to 20 single records across mixed resources in one call) is part of
|
|
260
|
+
the `hudu_read` surface; this server also registers the dedicated `hudu_fetch_many` tool from
|
|
261
|
+
the 0.12.0 CORE profile, with the secret-read policy applied to the batch's items on both
|
|
262
|
+
paths.
|
|
263
|
+
- **Error envelopes carry the status in a field.** The error `message` is the vendor's short
|
|
264
|
+
detail (or a fixed default), never the status or the dialed host; those travel in their own
|
|
265
|
+
fields, and a long upstream body is still replaced by the response shape, not served. The
|
|
266
|
+
envelope also carries the SDK's `category`, `retryable`, `operation`, `suggestedAction`,
|
|
267
|
+
`fieldErrors` and `details` where the error has them, `notSent` on a locally issued 429
|
|
268
|
+
cooldown refusal, and `location` on a `REDIRECT_BLOCKED` refusal; the status field is
|
|
269
|
+
`httpStatus`. A 401/403-class credential refusal collapses to the fixed `UNAUTHORIZED`
|
|
270
|
+
envelope on every tool — the vendor's own message never crosses the boundary, and the
|
|
271
|
+
credential-shaped keys are redacted out of the payload the same way as the success path.
|
|
272
|
+
Two honest corners: `details` may carry the SDK's own 256-char vendor body snippet, labelled
|
|
273
|
+
untrusted like the rest of the envelope, and the `location` of a `REDIRECT_BLOCKED` refusal
|
|
274
|
+
is the vendor's own target, served so the deployment can report or verify it.
|
|
275
|
+
|
|
276
|
+
Every client the server builds — the parent, the fail-closed placeholders and each
|
|
277
|
+
(origin, credential) scope — opts into four 0.12.0 behaviours:
|
|
278
|
+
|
|
279
|
+
- `profile: 'agent'`: the conservative agent preset for every option this server leaves unset
|
|
280
|
+
(1 retry, 240/min rate limit, concurrency 3, bounded resolution scans). The rate-limit bucket
|
|
281
|
+
is per client, i.e. per scope. The preset stays a deployment-wide constant with no env
|
|
282
|
+
override; the one deadline it would otherwise govern is pinned explicitly instead
|
|
283
|
+
(`HUDU_TIMEOUT_MS`, below), and the 429 now arms the cooldown gate instead of retrying
|
|
284
|
+
blindly.
|
|
285
|
+
- `cache: 'recommended'` — env-toggled as `HUDU_CACHE_PRESET` (default `on`): the
|
|
286
|
+
per-entity read-through TTL preset (1 h structural / 5 min mutable / 1 min base). Caches are
|
|
287
|
+
per client, so per scope. A write through the write or delete client does not clear the read
|
|
288
|
+
client's cache (separate instances), so a read after a mutation is TTL-bounded stale, never
|
|
289
|
+
unbounded. `off` disables the read-through cache: repeated reads re-dial Hudu.
|
|
290
|
+
- `blockPrivateHosts` on the per-scope clients: the literal-IP SSRF floor for CALLER-named
|
|
291
|
+
origins, fail-closed at client construction. It complements the `HUDU_ALLOWED_BASE_HOSTS`
|
|
292
|
+
hostname matcher (which alone can express wildcard suffixes) and holds even in
|
|
293
|
+
bring-your-own-origin mode, where that matcher is wide. The operator's own base — the
|
|
294
|
+
`HUDU_BASE_URL` under `stdio`, the default origin under `http` — is trusted at the process /
|
|
295
|
+
env boundary and is never floor-checked, so an on-prem install whose Hudu sits on a literal
|
|
296
|
+
private IP is unaffected by the upgrade. Deliberately no env override: it is a security
|
|
297
|
+
property, not a deployment preference.
|
|
298
|
+
- `cooldown: { enabled: true }` — env-tunable as `HUDU_COOLDOWN` (default `on`): the 429
|
|
299
|
+
cooldown gate; a call refused locally by the gate answers with `notSent: true` in the error
|
|
300
|
+
envelope, so a model does not read a local refusal as a vendor rejection of a sent call.
|
|
301
|
+
`off` retries a 429 per the profile instead of arming a local deadline.
|
|
302
|
+
|
|
303
|
+
The whole-call deadline is `HUDU_TIMEOUT_MS` (positive integer milliseconds, default `30000`):
|
|
304
|
+
it is always passed to the SDK explicitly, so it stays above the agent profile's 20 s
|
|
305
|
+
regardless of preset — the preset's 20 s would shorten a cold `tier:'index'` body-index walk
|
|
306
|
+
that the 0.9.2 deployment tolerated at the SDK's 30 s, and a cold-scope search must not start
|
|
307
|
+
timing out because of an upgrade.
|
|
308
|
+
|
|
309
|
+
These are deployment-wide, not per-caller: `HUDU_CACHE_PRESET`, `HUDU_COOLDOWN` and
|
|
310
|
+
`HUDU_TIMEOUT_MS` are env-tunable with the defaults above, while `profile` and
|
|
311
|
+
`blockPrivateHosts` stay constants.
|
|
312
|
+
|
|
313
|
+
|
|
179
314
|
## Development
|
|
180
315
|
|
|
181
316
|
```
|
|
317
|
+
git clone https://github.com/msoukhomlinov/iit-mcp-hudu.git
|
|
318
|
+
cd iit-mcp-hudu
|
|
182
319
|
npm install
|
|
320
|
+
npm run build
|
|
183
321
|
npm test
|
|
184
322
|
npm run dev
|
|
185
323
|
```
|
|
186
324
|
|
|
187
325
|
CI (`.github/workflows/ci.yml`) runs `typecheck`, `lint` and `test` on every push and pull request.
|
|
188
326
|
|
|
189
|
-
`node-hudu` is pinned to `0.
|
|
327
|
+
`node-hudu` is pinned to `0.13.1`. The catalog hash test detects metadata drift; upgrades require
|
|
190
328
|
review of the client authority, operation enums and redaction regressions. Docker uses `npm ci`.
|
|
191
329
|
|
|
192
330
|
## Deployment
|
|
@@ -195,6 +333,10 @@ review of the client authority, operation enums and redaction regressions. Docke
|
|
|
195
333
|
not call Hudu — otherwise an upstream outage would cycle the container, turning a partial failure
|
|
196
334
|
into a total one.
|
|
197
335
|
|
|
336
|
+
`GET /doctor` (same trust model as `/healthz`: unauthenticated, answered before the MCP handler,
|
|
337
|
+
zero Hudu requests) serves the same zero-wire credential-shape report the `doctor` subcommand
|
|
338
|
+
prints, so a deployment can check its loaded credential without a vendor dial.
|
|
339
|
+
|
|
198
340
|
The deployment definition is `docker-compose.yml`: its own Compose project, joining an existing
|
|
199
341
|
`app_net` external network (create it with `docker network create app_net`, or rename it in the
|
|
200
342
|
file). The listener is not published to the host, so put a TLS-terminating reverse proxy in front of
|
package/dist/config.d.ts
CHANGED
|
@@ -67,6 +67,15 @@ declare const schema: z.ZodObject<{
|
|
|
67
67
|
warn: "warn";
|
|
68
68
|
error: "error";
|
|
69
69
|
}>>;
|
|
70
|
+
HUDU_CACHE_PRESET: z.ZodDefault<z.ZodEnum<{
|
|
71
|
+
on: "on";
|
|
72
|
+
off: "off";
|
|
73
|
+
}>>;
|
|
74
|
+
HUDU_COOLDOWN: z.ZodDefault<z.ZodEnum<{
|
|
75
|
+
on: "on";
|
|
76
|
+
off: "off";
|
|
77
|
+
}>>;
|
|
78
|
+
HUDU_TIMEOUT_MS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
70
79
|
}, z.core.$strip>;
|
|
71
80
|
export type Config = z.infer<typeof schema>;
|
|
72
81
|
/**
|
package/dist/config.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as z from 'zod';
|
|
10
10
|
import { getCapability } from 'node-hudu/capabilities';
|
|
11
11
|
import { SEARCH_RESOURCES } from 'node-hudu/mcp';
|
|
12
|
+
import { CREDENTIAL_LOAD_HINT, detectCredentialShape } from './credential.js';
|
|
12
13
|
import { isWriteEffect } from './policy.js';
|
|
13
14
|
/**
|
|
14
15
|
* `stdio` is the local path (Claude Code, Claude Desktop, VS Code): one process, one credential,
|
|
@@ -182,8 +183,41 @@ const schema = z
|
|
|
182
183
|
HUDU_SECRET_READS: z.enum(SECRET_READ_POLICIES).default('deny'),
|
|
183
184
|
PORT: z.coerce.number().int().positive().max(65535).default(8787),
|
|
184
185
|
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
186
|
+
/**
|
|
187
|
+
* The read-through cache preset every client this server builds gets (SDK 0.12.0).
|
|
188
|
+
*
|
|
189
|
+
* The operator-facing toggle for the read-through cache. `on` (the default) selects the
|
|
190
|
+
* SDK's `recommended` per-entity TTL preset (1 h structural / 5 min mutable / 1 min base);
|
|
191
|
+
* `off` disables the read-through cache, so a repeated read re-dials Hudu instead of
|
|
192
|
+
* being served from the cache.
|
|
193
|
+
*/
|
|
194
|
+
HUDU_CACHE_PRESET: z.enum(['on', 'off']).default('on'),
|
|
195
|
+
/**
|
|
196
|
+
* The 429 cooldown gate (SDK 0.12.0).
|
|
197
|
+
*
|
|
198
|
+
* `on` (the default) arms a local deadline after a vendor 429 whose retries are SPENT (the
|
|
199
|
+
* agent profile's 1 retry, so the first such call makes two wire attempts): calls the gate
|
|
200
|
+
* then refuses locally, and the error envelope marks them `notSent: true` so a local
|
|
201
|
+
* refusal stays distinguishable from a vendor rejection of a sent call. `off`
|
|
202
|
+
* retries a 429 per the profile's retry budget instead of arming a local deadline.
|
|
203
|
+
*/
|
|
204
|
+
HUDU_COOLDOWN: z.enum(['on', 'off']).default('on'),
|
|
205
|
+
/**
|
|
206
|
+
* The whole-call deadline in milliseconds (SDK 0.12.0). Positive integer.
|
|
207
|
+
*
|
|
208
|
+
* Defaults to 30000 and is always passed to the SDK explicitly, so it wins over the
|
|
209
|
+
* agent profile's 20 s deadline regardless of preset; the default keeps the tolerance
|
|
210
|
+
* a cold `tier:'index'` body-index walk had before the 0.12.0 adoption.
|
|
211
|
+
*/
|
|
212
|
+
HUDU_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
|
|
185
213
|
})
|
|
186
214
|
.superRefine((c, ctx) => {
|
|
215
|
+
// Pre-dial integrity check (2026-10-07 Autotask incident class): a credential whose bytes were
|
|
216
|
+
// mangled by a quote-preserving load is refused HERE, before any client is constructed and so
|
|
217
|
+
// before the first dial. Refusal is transport-independent: `HUDU_API_KEY` under `http` is the
|
|
218
|
+
// default key for a request that brings none, and presenting a mangled one is the same
|
|
219
|
+
// incident on a different path.
|
|
220
|
+
checkCredentialShape(c.HUDU_API_KEY, ctx, 'HUDU_API_KEY');
|
|
187
221
|
// `stdio` has exactly one origin and one credential (the environment's), so it requires both.
|
|
188
222
|
// Under `http` both are optional defaults: a caller's `x-hudu-*` headers win, and the defaults
|
|
189
223
|
// fill in whatever a request leaves out (see `resolveHuduClient` for how they pair).
|
|
@@ -300,6 +334,34 @@ const schema = z
|
|
|
300
334
|
}
|
|
301
335
|
}
|
|
302
336
|
});
|
|
337
|
+
/**
|
|
338
|
+
* Apply the same check to `HUDU_API_KEY` here as to the per-request key under `http`.
|
|
339
|
+
*
|
|
340
|
+
* {@link CREDENTIAL_LOAD_HINT} — the env-load pitfall the refusal ends with — lives in
|
|
341
|
+
* `src/credential.ts` beside the anomaly vocabulary, so the boot error and the http refusal explain
|
|
342
|
+
* the same cause in the same words.
|
|
343
|
+
*
|
|
344
|
+
* Lives in a refinement rather than in the field's own `preprocess` so the issue carries the same
|
|
345
|
+
* `path` (the variable name) the operator already reads, and so a value that is BOTH blank and
|
|
346
|
+
* malformed reports only the blank case.
|
|
347
|
+
*
|
|
348
|
+
* The message never contains a character of the value: the anomaly names and the length are the
|
|
349
|
+
* whole diagnostic. That matters — the value is the secret, and this text is printed to the boot
|
|
350
|
+
* log (and, under `http`, would be too).
|
|
351
|
+
*/
|
|
352
|
+
function checkCredentialShape(value, ctx, path) {
|
|
353
|
+
if (value === undefined)
|
|
354
|
+
return;
|
|
355
|
+
const anomalies = detectCredentialShape(value);
|
|
356
|
+
if (anomalies.length === 0)
|
|
357
|
+
return;
|
|
358
|
+
ctx.addIssue({
|
|
359
|
+
code: 'custom',
|
|
360
|
+
path: [path],
|
|
361
|
+
message: `${path} has a value whose shape cannot be right (length ${value.length}, ${anomalies.join(', ')}); ` +
|
|
362
|
+
`the server refuses to boot rather than present a malformed credential to Hudu. ${CREDENTIAL_LOAD_HINT}`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
303
365
|
/**
|
|
304
366
|
* Parse and validate configuration from an environment-shaped record.
|
|
305
367
|
*
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credential.ts — the pre-dispatch shape gate for Hudu credential values, and the value-free
|
|
3
|
+
* description of a credential the operator-facing doctor reports.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists (2026-10-07 Autotask incident): a `.env` value written `HUDU_API_KEY='a$b'` was
|
|
6
|
+
* fed through a quote-preserving dotenv parser, so the quotes reached the wire as part of the
|
|
7
|
+
* secret. The vendor answered 401 and the operator read three opaque failures as a rotated key —
|
|
8
|
+
* on an account where retries risk a lock. Nothing in this package, and nothing in node-hudu
|
|
9
|
+
* 0.12.0, inspects the shape of a credential value before it is dialed, so that class of misload
|
|
10
|
+
* booted cleanly and dialed.
|
|
11
|
+
*
|
|
12
|
+
* This module closes that at the boundary. It never inspects, stores or reports a credential
|
|
13
|
+
* VALUE — only shape facts derived from it (a length, a two-character prefix, and which anomaly
|
|
14
|
+
* was seen). {@link describeCredential} and {@link detectCredentialShape} return metadata for a
|
|
15
|
+
* value caller; no field of either ever carries a character of the value itself, so both are safe
|
|
16
|
+
* to log and to serialize.
|
|
17
|
+
*/
|
|
18
|
+
export declare const CREDENTIAL_LOAD_HINT = "load the value the way the file documents: values written in single quotes (the .env.example style) are only unquoted by shell sourcing \u2014 `set -a; . ./.env; set +a` \u2014 and a quote-preserving parser such as a plain dotenv parse of the file feeds the quotes to the vendor; run `doctor` to confirm the shape";
|
|
19
|
+
/**
|
|
20
|
+
* The shape anomalies a mislanded credential file load produces, in the order they are reported.
|
|
21
|
+
*
|
|
22
|
+
* `surrounding_single_quotes` is the incident itself: a value written `KEY='a$b'` in a `.env` loses
|
|
23
|
+
* its quotes when the file is SOURCED by a shell, so a value that still carries them here was never
|
|
24
|
+
* shell-sourced (or was parsed by something that preserves them).
|
|
25
|
+
*/
|
|
26
|
+
export declare const CREDENTIAL_ANOMALIES: readonly ["surrounding_double_quotes", "surrounding_single_quotes", "embedded_quotes", "carriage_return", "newline", "edge_whitespace", "nul_byte"];
|
|
27
|
+
export type CredentialAnomaly = (typeof CREDENTIAL_ANOMALIES)[number];
|
|
28
|
+
/**
|
|
29
|
+
* Whether a shape anomaly means the server must refuse to dial rather than warn.
|
|
30
|
+
*
|
|
31
|
+
* Every anomaly in {@link CREDENTIAL_ANOMALIES} changes the bytes presented to the vendor, so
|
|
32
|
+
* every one of them is refused by both intake paths. The predicate is a function, not a literal
|
|
33
|
+
* `true` in the callers, so the decision lives in one place if it ever narrows.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isFatalAnomaly(anomaly: CredentialAnomaly): boolean;
|
|
36
|
+
/** The value-free shape description of one credential-shaped string. */
|
|
37
|
+
export interface CredentialShape {
|
|
38
|
+
/** Character count of the value as received. A count, never a character of the value. */
|
|
39
|
+
readonly length: number;
|
|
40
|
+
/**
|
|
41
|
+
* The first two characters, which identify the format for the operator (a Hudu API key starts
|
|
42
|
+
* `ey` — a base64url-encoded JWT). Two characters of an already-leaked-format credential say
|
|
43
|
+
* nothing; the other 500 do.
|
|
44
|
+
*/
|
|
45
|
+
readonly prefix2: string;
|
|
46
|
+
/** Every anomaly seen, in {@link CREDENTIAL_ANOMALIES} order. */
|
|
47
|
+
readonly anomalies: readonly CredentialAnomaly[];
|
|
48
|
+
}
|
|
49
|
+
/** Every anomaly present in `value`, in a fixed order. Never returns any part of the value. */
|
|
50
|
+
export declare function detectCredentialShape(value: string): readonly CredentialAnomaly[];
|
|
51
|
+
/**
|
|
52
|
+
* The shape facts an operator-facing surface may report for one credential field.
|
|
53
|
+
*
|
|
54
|
+
* `prefix2` is emitted for values of at least two characters and a single `<1>` otherwise, so the
|
|
55
|
+
* report never implies a one-character secret. The value itself is never returned.
|
|
56
|
+
*/
|
|
57
|
+
export declare function describeCredential(value: string | undefined): CredentialShape | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* The partial config one credential-bearing attempt produced, in the shape the operator's next
|
|
60
|
+
* step needs: whether the variable was set at all, and its value-free shape when it was.
|
|
61
|
+
*/
|
|
62
|
+
export interface CredentialReport {
|
|
63
|
+
readonly present: boolean;
|
|
64
|
+
readonly shape?: CredentialShape;
|
|
65
|
+
}
|
|
66
|
+
/** Describe one credential-bearing variable for a diagnostic surface. Never carries its value. */
|
|
67
|
+
export declare function reportCredential(env: Record<string, string | undefined>, name: string): CredentialReport;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credential.ts — the pre-dispatch shape gate for Hudu credential values, and the value-free
|
|
3
|
+
* description of a credential the operator-facing doctor reports.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists (2026-10-07 Autotask incident): a `.env` value written `HUDU_API_KEY='a$b'` was
|
|
6
|
+
* fed through a quote-preserving dotenv parser, so the quotes reached the wire as part of the
|
|
7
|
+
* secret. The vendor answered 401 and the operator read three opaque failures as a rotated key —
|
|
8
|
+
* on an account where retries risk a lock. Nothing in this package, and nothing in node-hudu
|
|
9
|
+
* 0.12.0, inspects the shape of a credential value before it is dialed, so that class of misload
|
|
10
|
+
* booted cleanly and dialed.
|
|
11
|
+
*
|
|
12
|
+
* This module closes that at the boundary. It never inspects, stores or reports a credential
|
|
13
|
+
* VALUE — only shape facts derived from it (a length, a two-character prefix, and which anomaly
|
|
14
|
+
* was seen). {@link describeCredential} and {@link detectCredentialShape} return metadata for a
|
|
15
|
+
* value caller; no field of either ever carries a character of the value itself, so both are safe
|
|
16
|
+
* to log and to serialize.
|
|
17
|
+
*/
|
|
18
|
+
export const CREDENTIAL_LOAD_HINT = 'load the value the way the file documents: values written in single quotes (the .env.example style) are only unquoted by shell sourcing — `set -a; . ./.env; set +a` — and a quote-preserving parser such as a plain dotenv parse of the file feeds the quotes to the vendor; run `doctor` to confirm the shape';
|
|
19
|
+
/**
|
|
20
|
+
* The shape anomalies a mislanded credential file load produces, in the order they are reported.
|
|
21
|
+
*
|
|
22
|
+
* `surrounding_single_quotes` is the incident itself: a value written `KEY='a$b'` in a `.env` loses
|
|
23
|
+
* its quotes when the file is SOURCED by a shell, so a value that still carries them here was never
|
|
24
|
+
* shell-sourced (or was parsed by something that preserves them).
|
|
25
|
+
*/
|
|
26
|
+
export const CREDENTIAL_ANOMALIES = [
|
|
27
|
+
'surrounding_double_quotes',
|
|
28
|
+
'surrounding_single_quotes',
|
|
29
|
+
'embedded_quotes',
|
|
30
|
+
'carriage_return',
|
|
31
|
+
'newline',
|
|
32
|
+
'edge_whitespace',
|
|
33
|
+
'nul_byte',
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Whether a shape anomaly means the server must refuse to dial rather than warn.
|
|
37
|
+
*
|
|
38
|
+
* Every anomaly in {@link CREDENTIAL_ANOMALIES} changes the bytes presented to the vendor, so
|
|
39
|
+
* every one of them is refused by both intake paths. The predicate is a function, not a literal
|
|
40
|
+
* `true` in the callers, so the decision lives in one place if it ever narrows.
|
|
41
|
+
*/
|
|
42
|
+
export function isFatalAnomaly(anomaly) {
|
|
43
|
+
return CREDENTIAL_ANOMALIES.includes(anomaly);
|
|
44
|
+
}
|
|
45
|
+
/** Every anomaly present in `value`, in a fixed order. Never returns any part of the value. */
|
|
46
|
+
export function detectCredentialShape(value) {
|
|
47
|
+
const anomalies = [];
|
|
48
|
+
// The incident, first: quotes preserved by a parser that should have consumed them.
|
|
49
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"'))
|
|
50
|
+
anomalies.push('surrounding_double_quotes');
|
|
51
|
+
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'"))
|
|
52
|
+
anomalies.push('surrounding_single_quotes');
|
|
53
|
+
// A quote that survived inside the value: a shell-sourced value cannot contain a bare one, and
|
|
54
|
+
// a shell that never ran leaves them where a parser put them.
|
|
55
|
+
if (value.includes('"') || value.includes("'"))
|
|
56
|
+
anomalies.push('embedded_quotes');
|
|
57
|
+
// CRLF is the canonical Windows-edited .env contamination: the CRLF and everything after it
|
|
58
|
+
// reaches the vendor as part of the secret. (A per-request header cannot carry one — Node's
|
|
59
|
+
// `Headers` strips it — so this branch is reachable only through the server-held value.)
|
|
60
|
+
if (value.includes('\r'))
|
|
61
|
+
anomalies.push('carriage_return');
|
|
62
|
+
if (value.includes('\n'))
|
|
63
|
+
anomalies.push('newline');
|
|
64
|
+
// A trailing space from `KEY=value ` is invisible in every editor and is presented verbatim.
|
|
65
|
+
// `trim()` also catches a tab, which survives a header hop even though Node's Headers strips
|
|
66
|
+
// space at the edges of a header value.
|
|
67
|
+
if (value.length > 0 && value !== value.trim())
|
|
68
|
+
anomalies.push('edge_whitespace');
|
|
69
|
+
// NUL is not a legal header character: Node's `Headers.set` THROWS on a value carrying one, and
|
|
70
|
+
// that throw happens before any auth strategy runs, so a request with such a key answers with a
|
|
71
|
+
// framework error instead of a typed refusal. Flagged here so the server-held value is refused at
|
|
72
|
+
// boot, where the operator can act on it.
|
|
73
|
+
if (value.includes('\u0000'))
|
|
74
|
+
anomalies.push('nul_byte');
|
|
75
|
+
return anomalies;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The shape facts an operator-facing surface may report for one credential field.
|
|
79
|
+
*
|
|
80
|
+
* `prefix2` is emitted for values of at least two characters and a single `<1>` otherwise, so the
|
|
81
|
+
* report never implies a one-character secret. The value itself is never returned.
|
|
82
|
+
*/
|
|
83
|
+
export function describeCredential(value) {
|
|
84
|
+
if (value === undefined)
|
|
85
|
+
return undefined;
|
|
86
|
+
return {
|
|
87
|
+
length: value.length,
|
|
88
|
+
prefix2: value.length >= 2 ? value.slice(0, 2) : '<1>',
|
|
89
|
+
anomalies: detectCredentialShape(value),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Describe one credential-bearing variable for a diagnostic surface. Never carries its value. */
|
|
93
|
+
export function reportCredential(env, name) {
|
|
94
|
+
const raw = env[name];
|
|
95
|
+
// `''` is the .env.example placeholder an operator forgot to fill in: "not set", exactly as the
|
|
96
|
+
// boot schema normalises it, rather than a zero-length credential.
|
|
97
|
+
if (raw === undefined || raw.trim() === '')
|
|
98
|
+
return { present: false };
|
|
99
|
+
return { present: true, shape: describeCredential(raw) };
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=credential.js.map
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor.ts — the zero-wire credential/config preflight.
|
|
3
|
+
*
|
|
4
|
+
* The problem it solves is the expensive one: today the only way to confirm that a credential
|
|
5
|
+
* loaded intact is to dial Hudu, and against a vendor account where repeated auth failures risk a
|
|
6
|
+
* lock (2026-10-07 Autotask incident) that confirmation is the most expensive call the deployment
|
|
7
|
+
* can make. `doctor` answers the same question — "is the value I loaded shaped like the credential
|
|
8
|
+
* the vendor issued, and is it aimed at the origin I meant?" — from the process's own environment,
|
|
9
|
+
* with ZERO vendor requests, by construction: this module imports no Hudu client and reads no
|
|
10
|
+
* network, so there is nothing it could dial even if a value invited it.
|
|
11
|
+
*
|
|
12
|
+
* It reports shape, never value: a length, a two-character prefix and which anomalies were seen.
|
|
13
|
+
* The contract it feeds (README / `.env.example`): run `doctor` before a live session — a green
|
|
14
|
+
* doctor means any later 401 is vendor-side (rotated, revoked or role-restricted key), not a
|
|
15
|
+
* mangled load.
|
|
16
|
+
*/
|
|
17
|
+
import type { Config } from './config.js';
|
|
18
|
+
/** How one credential field's shape check came out. */
|
|
19
|
+
export type DoctorCheckStatus = 'absent' | 'ok' | 'malformed';
|
|
20
|
+
/** The shape report for one credential-bearing variable: metadata only, never a value. */
|
|
21
|
+
export interface DoctorCredentialCheck {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly status: DoctorCheckStatus;
|
|
24
|
+
readonly length?: number;
|
|
25
|
+
readonly prefix2?: string;
|
|
26
|
+
readonly shapeOk?: boolean;
|
|
27
|
+
readonly anomalies?: readonly string[];
|
|
28
|
+
/**
|
|
29
|
+
* Whether the intake path this variable feeds would refuse it. Under `stdio` `HUDU_API_KEY` is
|
|
30
|
+
* required, so an absent one is as fatal as a malformed one; under `http` it is an optional
|
|
31
|
+
* default and absence is fine.
|
|
32
|
+
*/
|
|
33
|
+
readonly blocks?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** The shape report for one origin value: host and scheme only, never a path or a query. */
|
|
36
|
+
export interface DoctorBaseCheck {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly present: boolean;
|
|
39
|
+
/** `origin` (scheme + host + port), never the full URL. The origin is what the SDK dials. */
|
|
40
|
+
readonly origin?: string;
|
|
41
|
+
readonly resolvable: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** The whole zero-wire report. `ok: false` means at least one field would be refused. */
|
|
44
|
+
export interface DoctorReport {
|
|
45
|
+
readonly ok: boolean;
|
|
46
|
+
readonly transport: Config['MCP_TRANSPORT'];
|
|
47
|
+
readonly dialed: number;
|
|
48
|
+
readonly credentials: readonly DoctorCredentialCheck[];
|
|
49
|
+
readonly bases: readonly DoctorBaseCheck[];
|
|
50
|
+
readonly notes: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the doctor report for an already-validated config.
|
|
54
|
+
*
|
|
55
|
+
* Takes the parsed `Config` rather than a raw environment on purpose: `loadConfig` has already
|
|
56
|
+
* refused a blank/missing/origin-invalid configuration — and, at boot, a malformed credential — so
|
|
57
|
+
* reaching here means every structural question is settled and only the SHAPE facts remain.
|
|
58
|
+
*
|
|
59
|
+
* A malformed credential therefore normally reaches the operator as the boot refusal (the CLI
|
|
60
|
+
* prints it verbatim, exit 1, and it names the anomaly and the load recipe). This report is the
|
|
61
|
+
* same facts in structured form, for a config that is already shaped like a working one; the two
|
|
62
|
+
* routes are tested together in `test/doctor.test.ts`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function runDoctor(config: Config): DoctorReport;
|