@aria-framework/ai 0.26.0 → 0.26.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/lmxVerify.js CHANGED
@@ -1,402 +1,402 @@
1
- /**
2
- * Does this supervisor actually work? — the four questions, asked in the only order they can be.
3
- *
4
- * WHY THIS EXISTS. A supervisor was saved correctly twice and looked identical both times, while the
5
- * engines key sat in the status token's field. The listener answered 401, and the only place that
6
- * surfaced was an HTTP code behind a button — a number that names neither credential. Saving
7
- * something and finding out later whether it works is the shape of that whole failure.
8
- *
9
- * THE ORDER IS FORCED, and each step's failure means something the next cannot tell you:
10
- *
11
- * 1. reachable — nothing below it means anything if the machine does not answer
12
- * 2. certificate — fails BEFORE any credential leaves this process, which is why its message
13
- * says so: a pin mismatch is the whole story, and the tokens are not suspect
14
- * 3. status token — FREE. Reading the document is exactly what this credential authorises, so
15
- * whether it works is learned as a side effect of asking
16
- * 4. engines key — authorises INFERENCE, and no amount of status reading exercises it
17
- *
18
- * CHECK 4 SPENDS NOTHING. The obvious way to test an inference credential is to run an inference;
19
- * asking the engine's own `/models` instead authenticates against the same route with the same
20
- * bearer and costs no tokens at all. An operator should never have to weigh "is my key right?"
21
- * against what the answer costs.
22
- *
23
- * NOTHING HERE READS A DATABASE OR A KEYSTORE. Every credential arrives as an argument, which is
24
- * what lets the same function serve an admin screen, a boot check and a test with a stub fetch.
25
- * Where the values come from is the consuming app's business.
26
- *
27
- * WHAT IS NEVER DONE: a check is never reported as passed because it probably would have. When the
28
- * engine list does not arrive, the engines key reads `not checked` rather than a tick — it is the
29
- * credential whose failure stays invisible until a job runs, so a tick meaning "probably" is worse
30
- * there than no tick at all.
31
- */
32
-
33
- 'use strict';
34
-
35
- const crypto = require('crypto');
36
-
37
- const DEFAULT_TIMEOUT_MS = 6000;
38
-
39
- /** How long a verification stays on the page before it is stale enough to be misleading. */
40
- const REPORT_TTL_MS = 10 * 60 * 1000;
41
-
42
- /** Certificates inside this window are called out — an expired pin takes the stack out silently. */
43
- const EXPIRY_WARN_DAYS = 30;
44
-
45
- /**
46
- * The last verification per instance, so the report survives the redirect that follows a save.
47
- *
48
- * NOT A FLASH. The flash partial escapes to a single line, and a per-check report is exactly the
49
- * kind of result the partial's own notes warn about losing — "the only place that output appears,
50
- * and a message that erases itself after three seconds would lose it with no way back". Keeping it
51
- * here means it stays readable until something supersedes it, which is also what makes it useful
52
- * outside the moment of saving.
53
- */
54
- const _reports = new Map();
55
-
56
- function remember(instance, report) {
57
- _reports.set(String(instance), report);
58
- return report;
59
- }
60
-
61
- /** The last report for an instance, or null once it is old enough to mislead. */
62
- function lastFor(instance) {
63
- const r = _reports.get(String(instance));
64
- if (!r) return null;
65
- if (Date.now() - new Date(r.at).getTime() > REPORT_TTL_MS) {
66
- _reports.delete(String(instance));
67
- return null;
68
- }
69
- return r;
70
- }
71
-
72
- function forget(instance) { _reports.delete(String(instance)); }
73
-
74
- /** Only for tests — the cache is process-wide and would otherwise leak between them. */
75
- function _reset() { _reports.clear(); }
76
-
77
- // ── classifying what went wrong ────────────────────────────────────────────────────────────────
78
- //
79
- // fetch() reports every transport failure as the same "fetch failed", with the real reason on
80
- // `cause`. Collapsing those into one message would put a certificate mismatch and an unplugged
81
- // network cable behind identical words, which is the confusion this whole module exists to end.
82
-
83
- const TLS_CODES = new Set([
84
- 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'SELF_SIGNED_CERT_IN_CHAIN', 'DEPTH_ZERO_SELF_SIGNED_CERT',
85
- 'CERT_HAS_EXPIRED', 'CERT_NOT_YET_VALID', 'ERR_TLS_CERT_ALTNAME_INVALID',
86
- 'CERT_SIGNATURE_FAILURE', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', 'ERR_SSL_WRONG_VERSION_NUMBER'
87
- ]);
88
-
89
- function causeOf(err) {
90
- let e = err;
91
- for (let i = 0; i < 5 && e && e.cause; i += 1) e = e.cause;
92
- return e || err;
93
- }
94
-
95
- function classify(err) {
96
- const c = causeOf(err);
97
- const code = (c && c.code) || '';
98
- if (err && err.name === 'TimeoutError') return { kind: 'unreachable', code: 'ETIMEDOUT' };
99
- if (TLS_CODES.has(code)) return { kind: 'tls', code };
100
- if (/certificate|self.signed|SSL/i.test(String(c && c.message))) return { kind: 'tls', code: code || 'TLS' };
101
- return { kind: 'unreachable', code: code || 'unknown' };
102
- }
103
-
104
- // ── the pinned certificate, read locally ───────────────────────────────────────────────────────
105
-
106
- /**
107
- * What we are pinning, without asking anybody. The fingerprint and expiry are properties of the PEM
108
- * in front of us, so they are known even when the stack is off — which is when an operator most
109
- * needs to be told the pin expires in a fortnight.
110
- */
111
- function readPin(pem) {
112
- if (!pem || !String(pem).trim()) return { present: false };
113
- try {
114
- const x = new crypto.X509Certificate(String(pem));
115
- const expires = new Date(x.validTo);
116
- const days = Math.floor((expires.getTime() - Date.now()) / 86400000);
117
- return {
118
- present: true,
119
- valid: true,
120
- // Short form, because the whole digest is unreadable and nobody compares 32 bytes by eye.
121
- fingerprint: shortPrint(x.fingerprint256),
122
- expires,
123
- expiresDays: days,
124
- expired: days < 0,
125
- expiringSoon: days >= 0 && days <= EXPIRY_WARN_DAYS
126
- };
127
- } catch (e) {
128
- return { present: true, valid: false, error: e.message };
129
- }
130
- }
131
-
132
- function shortPrint(fp) {
133
- const parts = String(fp || '').split(':');
134
- if (parts.length < 4) return String(fp || '');
135
- return `${parts.slice(0, 2).join(':')}…${parts[parts.length - 1]}`;
136
- }
137
-
138
- function fmtDate(d) {
139
- return d instanceof Date && !isNaN(d) ? d.toISOString().slice(0, 10) : 'unknown';
140
- }
141
-
142
- // ── the checks ─────────────────────────────────────────────────────────────────────────────────
143
-
144
- const pass = (key, text) => ({ key, status: 'pass', text });
145
- const fail = (key, text) => ({ key, status: 'fail', text });
146
- const skip = (key, text) => ({ key, status: 'skip', text });
147
-
148
- /**
149
- * Run the four (occasionally five) checks against a supervisor.
150
- *
151
- * @param {object} o
152
- * instance the id this stack must report as itself
153
- * statusUrl the status listener
154
- * statusToken bearer for the status document
155
- * caCert the certificate to pin, PEM
156
- * enginesKey bearer for inference — checked only when there is an engine to check it against
157
- * engineName an adopted engine to authenticate against; omitted, the first healthy one is used
158
- * engineId its stable id, when the row has one — matched instead of the name (renames)
159
- * fetchImpl test seam; the pinned transport is used when absent
160
- */
161
- async function verify(o = {}) {
162
- const instance = String(o.instance || '');
163
- const statusUrl = String(o.statusUrl || '');
164
- const timeoutMs = Number(o.timeoutMs) || DEFAULT_TIMEOUT_MS;
165
- const checks = [];
166
- const at = new Date().toISOString();
167
-
168
- const pin = readPin(o.caCert);
169
- const host = hostOf(statusUrl);
170
-
171
- // ── 1 + 2 + 3: one request answers all three, because they fail at different layers of it ──
172
- let doc = null;
173
- let found = null; // the engine list, when the document arrived
174
- try {
175
- const started = Date.now();
176
- const res = await statusFetch(o, statusUrl, timeoutMs);
177
- const ms = Date.now() - started;
178
-
179
- checks.push(pass('reachable', `reached ${host} in ${ms} ms`));
180
- checks.push(certCheck(pin, true));
181
-
182
- if (res.status === 401 || res.status === 403) {
183
- checks.push(fail('status_token',
184
- `status token rejected — the listener answered ${res.status}`));
185
- checks.push(skip('engines_key', 'engines key not checked — needs the engine list'));
186
- return finish(instance, at, checks, null, found);
187
- }
188
- if (!res.ok) {
189
- checks.push(fail('status_token', `the listener answered ${res.status}`));
190
- checks.push(skip('engines_key', 'engines key not checked — needs the engine list'));
191
- return finish(instance, at, checks, null, found);
192
- }
193
-
194
- try {
195
- doc = await res.json();
196
- } catch (e) {
197
- checks.push(fail('status_token', 'the listener answered, but not with a status document'));
198
- checks.push(skip('engines_key', 'engines key not checked — needs the engine list'));
199
- return finish(instance, at, checks, null, found);
200
- }
201
-
202
- const engines = Array.isArray(doc && doc.engines) ? doc.engines : [];
203
- checks.push(pass('status_token',
204
- `status token accepted · ${engines.length} engine${engines.length === 1 ? '' : 's'} reported`));
205
-
206
- // THE DOCUMENT COMES BACK WITH THE VERDICT. Verifying already costs a status read, and that read
207
- // is the same one the engine picker used to make on demand — so carrying the list here lets the
208
- // panel render its engines from what we just fetched instead of asking again from the browser.
209
- // It is also what removes the "Discover engines" button: a list you have to press for is a list
210
- // that is stale by definition, and pressing it was where the 401 used to hide.
211
- found = engines;
212
-
213
- // IDENTITY, not connectivity. Engine names collide across deployments — `analysis` exists on
214
- // every stack — so a status URL pointed at the wrong machine answers plausibly and sends work
215
- // somewhere else. Only raised when it is actually wrong; a matching id needs no line.
216
- if (doc && doc.instance && String(doc.instance) !== instance) {
217
- checks.push(fail('instance',
218
- `this stack reports itself as “${doc.instance}”, not “${instance}” — the address points at a different deployment`));
219
- // AND ITS ENGINES ARE NOT HANDED BACK (0.25.0). The apps' scheduled refresh feeds
220
- // report.engines to rekeyPlan; returning another deployment's list wrote THAT stack's engine
221
- // ids onto this stack's rows — irreversibly, since a row with an id is never re-pointed.
222
- // `null` is "no document for this stack", which every consumer treats as "write nothing".
223
- found = null;
224
- }
225
-
226
- checks.push(await enginesKeyCheck(o, engines, timeoutMs));
227
- return finish(instance, at, checks, null, found);
228
- } catch (err) {
229
- const { kind, code } = classify(err);
230
- if (kind === 'tls') {
231
- // NOTHING WAS SENT. The handshake failed, so neither credential left this process — worth
232
- // saying, because it means the tokens are not what to go and look at.
233
- checks.push(pass('reachable', `reached ${host}`));
234
- checks.push(fail('certificate', certFailText(pin, code)));
235
- checks.push(skip('status_token', 'status token not sent — the connection was refused first'));
236
- checks.push(skip('engines_key', 'engines key not sent'));
237
- return finish(instance, at, checks, null, found);
238
- }
239
- checks.push(fail('reachable', `no answer from ${host} (${code})`));
240
- checks.push(skip('certificate', 'certificate not checked'));
241
- checks.push(skip('status_token', 'status token not checked'));
242
- checks.push(skip('engines_key', 'engines key not checked'));
243
- return finish(instance, at, checks, pin, found);
244
- }
245
- }
246
-
247
- function hostOf(url) {
248
- try { const u = new URL(url); return u.host; } catch (e) { return url || 'the listener'; }
249
- }
250
-
251
- /** The status read, over the pinned transport unless a test supplies its own fetch. */
252
- async function statusFetch(o, statusUrl, timeoutMs) {
253
- const headers = { Accept: 'application/json' };
254
- if (o.statusToken) headers.Authorization = `Bearer ${o.statusToken}`;
255
- return send(o, statusUrl, headers, timeoutMs);
256
- }
257
-
258
- /**
259
- * One request, over the pinned transport.
260
- *
261
- * THE DISPATCHER IS HALF THE TRANSPORT. `lmxTransport(ca)` returns `{ fetch, dispatcher }` and the
262
- * certificate lives on the DISPATCHER — taking only `.fetch` and calling it leaves the pin behind,
263
- * and the request goes out against the system trust store instead. Against a self-signed stack that
264
- * surfaces as DEPTH_ZERO_SELF_SIGNED_CERT, which reads exactly like a genuine certificate mismatch:
265
- * a verifier that reported "the certificate did not match" for a perfectly good certificate, which
266
- * is a worse failure than the one it was written to catch.
267
- */
268
- async function send(o, url, headers, timeoutMs) {
269
- const opts = { headers, signal: AbortSignal.timeout(timeoutMs) };
270
- if (o.fetchImpl) return o.fetchImpl(url, opts);
271
- const { lmxTransport } = require('./providers/lmxTransport');
272
- const t = lmxTransport(o.caCert || null);
273
- if (t.dispatcher) opts.dispatcher = t.dispatcher;
274
- return t.fetch(url, opts);
275
- }
276
-
277
- function certCheck(pin, handshakeOk) {
278
- if (!pin.present) {
279
- // Not a failure: a stack on loopback, or one behind a certificate the system already trusts,
280
- // needs no pin. Saying "none pinned" is the true statement; a tick would claim a check ran.
281
- return skip('certificate', 'no certificate pinned — the system trust store was used');
282
- }
283
- if (!pin.valid) return fail('certificate', `the pinned certificate could not be read (${pin.error})`);
284
- if (pin.expired) {
285
- return fail('certificate',
286
- `the pinned certificate EXPIRED on ${fmtDate(pin.expires)} — every call fails at the handshake`);
287
- }
288
- const base = `certificate matched the pin · ${pin.fingerprint} · expires ${fmtDate(pin.expires)}`;
289
- if (pin.expiringSoon) {
290
- return { key: 'certificate', status: 'warn', text: `${base} — ${pin.expiresDays} days left` };
291
- }
292
- return handshakeOk ? pass('certificate', base) : skip('certificate', base);
293
- }
294
-
295
- function certFailText(pin, code) {
296
- if (pin.present && pin.expired) {
297
- return `the pinned certificate expired on ${fmtDate(pin.expires)} (${code})`;
298
- }
299
- if (code === 'CERT_HAS_EXPIRED') return 'the stack is presenting an expired certificate';
300
- if (code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
301
- return 'the certificate is not valid for this address — check the hostname';
302
- }
303
- if (!pin.present) {
304
- return 'the stack presented a certificate this machine does not trust, and none is pinned here';
305
- }
306
- return `the stack presented a different certificate from the one pinned here (${code})`;
307
- }
308
-
309
- /**
310
- * Check 4 — WITHOUT SPENDING ANYTHING.
311
- *
312
- * The engines key is a bearer on the engine's own OpenAI-compatible API, so asking that API to list
313
- * its models authenticates against exactly the route inference uses. A completion would prove the
314
- * same thing and bill for the privilege.
315
- */
316
- async function enginesKeyCheck(o, engines, timeoutMs) {
317
- if (!o.enginesKey) {
318
- return skip('engines_key', 'no engines key set — engines are being called unauthenticated');
319
- }
320
- const { findEngine } = require('./providers/lmxDiscovery');
321
- const wanted = (o.engineId || o.engineName)
322
- ? findEngine(engines, { id: o.engineId || null, name: o.engineName })
323
- : engines.find((e) => e && e.state === 'healthy' && e.url);
324
- if (!wanted || !wanted.url) {
325
- return skip('engines_key',
326
- o.engineName
327
- ? `engines key not checked — “${o.engineName}” is not currently healthy`
328
- : 'engines key not checked — no healthy engine to authenticate against');
329
- }
330
- try {
331
- const url = `${String(wanted.url).replace(/\/+$/, '')}/models`;
332
- const res = await send(o, url,
333
- { Accept: 'application/json', Authorization: `Bearer ${o.enginesKey}` }, timeoutMs);
334
- if (res.status === 401 || res.status === 403) {
335
- return fail('engines_key',
336
- `engines key rejected by ${wanted.name} — inference calls will fail with ${res.status}`);
337
- }
338
- if (!res.ok) {
339
- return skip('engines_key', `engines key not confirmed — ${wanted.name} answered ${res.status}`);
340
- }
341
- return pass('engines_key', `engines key accepted · checked against ${wanted.name}`);
342
- } catch (err) {
343
- const { code } = classify(err);
344
- return skip('engines_key', `engines key not checked — ${wanted.name} did not answer (${code})`);
345
- }
346
- }
347
-
348
- // ── the verdict ────────────────────────────────────────────────────────────────────────────────
349
-
350
- function finish(instance, at, checks, pinForOffline, engines) {
351
- // A certificate that expires soon is worth saying even when nothing could be reached, because it
352
- // is knowable from the PEM alone and is exactly the failure nobody sees coming.
353
- if (pinForOffline && pinForOffline.present && pinForOffline.valid
354
- && (pinForOffline.expired || pinForOffline.expiringSoon)) {
355
- const i = checks.findIndex((c) => c.key === 'certificate');
356
- if (i > -1) checks[i] = certCheck(pinForOffline, false);
357
- }
358
-
359
- const failed = checks.filter((c) => c.status === 'fail');
360
- const warned = checks.filter((c) => c.status === 'warn');
361
- const ok = failed.length === 0;
362
-
363
- return {
364
- ok,
365
- level: failed.length ? (failed.some((c) => c.key === 'certificate') ? 'bad' : 'warn')
366
- : (warned.length ? 'warn' : 'ok'),
367
- headline: headlineFor(failed, warned),
368
- at,
369
- instance,
370
- checks,
371
- // Null means "we never got a document", which is NOT the same as a stack with no engines — the
372
- // panel must be able to say "not checked" rather than "no engines", because those send an
373
- // operator to entirely different places.
374
- engines: engines || null
375
- };
376
- }
377
-
378
- /**
379
- * ONE SENTENCE NAMING THE CAUSE, for the flash. "Saved, but the status token is being rejected"
380
- * is a different instruction from "saved, but the stack did not answer" — the first sends somebody
381
- * to a credential, the second to a machine, and "401" sent them to neither.
382
- */
383
- function headlineFor(failed, warned) {
384
- if (!failed.length) {
385
- return warned.length ? warned[0].text : 'connected, certificate pinned, both credentials accepted';
386
- }
387
- const first = failed[0];
388
- switch (first.key) {
389
- case 'reachable': return 'the stack did not answer — its settings are stored and will be used when it is back';
390
- case 'certificate': return 'the certificate did not match — nothing was sent';
391
- case 'status_token': return 'the status token is being rejected — engines cannot be listed until it is corrected';
392
- case 'instance': return 'this address points at a different deployment';
393
- case 'engines_key': return 'the engines key is being rejected — inference calls will fail';
394
- default: return first.text;
395
- }
396
- }
397
-
398
- module.exports = {
399
- verify, remember, lastFor, forget, readPin,
400
- DEFAULT_TIMEOUT_MS, EXPIRY_WARN_DAYS, REPORT_TTL_MS,
401
- _reset, _classify: classify, _shortPrint: shortPrint
402
- };
1
+ /**
2
+ * Does this supervisor actually work? — the four questions, asked in the only order they can be.
3
+ *
4
+ * WHY THIS EXISTS. A supervisor was saved correctly twice and looked identical both times, while the
5
+ * engines key sat in the status token's field. The listener answered 401, and the only place that
6
+ * surfaced was an HTTP code behind a button — a number that names neither credential. Saving
7
+ * something and finding out later whether it works is the shape of that whole failure.
8
+ *
9
+ * THE ORDER IS FORCED, and each step's failure means something the next cannot tell you:
10
+ *
11
+ * 1. reachable — nothing below it means anything if the machine does not answer
12
+ * 2. certificate — fails BEFORE any credential leaves this process, which is why its message
13
+ * says so: a pin mismatch is the whole story, and the tokens are not suspect
14
+ * 3. status token — FREE. Reading the document is exactly what this credential authorises, so
15
+ * whether it works is learned as a side effect of asking
16
+ * 4. engines key — authorises INFERENCE, and no amount of status reading exercises it
17
+ *
18
+ * CHECK 4 SPENDS NOTHING. The obvious way to test an inference credential is to run an inference;
19
+ * asking the engine's own `/models` instead authenticates against the same route with the same
20
+ * bearer and costs no tokens at all. An operator should never have to weigh "is my key right?"
21
+ * against what the answer costs.
22
+ *
23
+ * NOTHING HERE READS A DATABASE OR A KEYSTORE. Every credential arrives as an argument, which is
24
+ * what lets the same function serve an admin screen, a boot check and a test with a stub fetch.
25
+ * Where the values come from is the consuming app's business.
26
+ *
27
+ * WHAT IS NEVER DONE: a check is never reported as passed because it probably would have. When the
28
+ * engine list does not arrive, the engines key reads `not checked` rather than a tick — it is the
29
+ * credential whose failure stays invisible until a job runs, so a tick meaning "probably" is worse
30
+ * there than no tick at all.
31
+ */
32
+
33
+ 'use strict';
34
+
35
+ const crypto = require('crypto');
36
+
37
+ const DEFAULT_TIMEOUT_MS = 6000;
38
+
39
+ /** How long a verification stays on the page before it is stale enough to be misleading. */
40
+ const REPORT_TTL_MS = 10 * 60 * 1000;
41
+
42
+ /** Certificates inside this window are called out — an expired pin takes the stack out silently. */
43
+ const EXPIRY_WARN_DAYS = 30;
44
+
45
+ /**
46
+ * The last verification per instance, so the report survives the redirect that follows a save.
47
+ *
48
+ * NOT A FLASH. The flash partial escapes to a single line, and a per-check report is exactly the
49
+ * kind of result the partial's own notes warn about losing — "the only place that output appears,
50
+ * and a message that erases itself after three seconds would lose it with no way back". Keeping it
51
+ * here means it stays readable until something supersedes it, which is also what makes it useful
52
+ * outside the moment of saving.
53
+ */
54
+ const _reports = new Map();
55
+
56
+ function remember(instance, report) {
57
+ _reports.set(String(instance), report);
58
+ return report;
59
+ }
60
+
61
+ /** The last report for an instance, or null once it is old enough to mislead. */
62
+ function lastFor(instance) {
63
+ const r = _reports.get(String(instance));
64
+ if (!r) return null;
65
+ if (Date.now() - new Date(r.at).getTime() > REPORT_TTL_MS) {
66
+ _reports.delete(String(instance));
67
+ return null;
68
+ }
69
+ return r;
70
+ }
71
+
72
+ function forget(instance) { _reports.delete(String(instance)); }
73
+
74
+ /** Only for tests — the cache is process-wide and would otherwise leak between them. */
75
+ function _reset() { _reports.clear(); }
76
+
77
+ // ── classifying what went wrong ────────────────────────────────────────────────────────────────
78
+ //
79
+ // fetch() reports every transport failure as the same "fetch failed", with the real reason on
80
+ // `cause`. Collapsing those into one message would put a certificate mismatch and an unplugged
81
+ // network cable behind identical words, which is the confusion this whole module exists to end.
82
+
83
+ const TLS_CODES = new Set([
84
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'SELF_SIGNED_CERT_IN_CHAIN', 'DEPTH_ZERO_SELF_SIGNED_CERT',
85
+ 'CERT_HAS_EXPIRED', 'CERT_NOT_YET_VALID', 'ERR_TLS_CERT_ALTNAME_INVALID',
86
+ 'CERT_SIGNATURE_FAILURE', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', 'ERR_SSL_WRONG_VERSION_NUMBER'
87
+ ]);
88
+
89
+ function causeOf(err) {
90
+ let e = err;
91
+ for (let i = 0; i < 5 && e && e.cause; i += 1) e = e.cause;
92
+ return e || err;
93
+ }
94
+
95
+ function classify(err) {
96
+ const c = causeOf(err);
97
+ const code = (c && c.code) || '';
98
+ if (err && err.name === 'TimeoutError') return { kind: 'unreachable', code: 'ETIMEDOUT' };
99
+ if (TLS_CODES.has(code)) return { kind: 'tls', code };
100
+ if (/certificate|self.signed|SSL/i.test(String(c && c.message))) return { kind: 'tls', code: code || 'TLS' };
101
+ return { kind: 'unreachable', code: code || 'unknown' };
102
+ }
103
+
104
+ // ── the pinned certificate, read locally ───────────────────────────────────────────────────────
105
+
106
+ /**
107
+ * What we are pinning, without asking anybody. The fingerprint and expiry are properties of the PEM
108
+ * in front of us, so they are known even when the stack is off — which is when an operator most
109
+ * needs to be told the pin expires in a fortnight.
110
+ */
111
+ function readPin(pem) {
112
+ if (!pem || !String(pem).trim()) return { present: false };
113
+ try {
114
+ const x = new crypto.X509Certificate(String(pem));
115
+ const expires = new Date(x.validTo);
116
+ const days = Math.floor((expires.getTime() - Date.now()) / 86400000);
117
+ return {
118
+ present: true,
119
+ valid: true,
120
+ // Short form, because the whole digest is unreadable and nobody compares 32 bytes by eye.
121
+ fingerprint: shortPrint(x.fingerprint256),
122
+ expires,
123
+ expiresDays: days,
124
+ expired: days < 0,
125
+ expiringSoon: days >= 0 && days <= EXPIRY_WARN_DAYS
126
+ };
127
+ } catch (e) {
128
+ return { present: true, valid: false, error: e.message };
129
+ }
130
+ }
131
+
132
+ function shortPrint(fp) {
133
+ const parts = String(fp || '').split(':');
134
+ if (parts.length < 4) return String(fp || '');
135
+ return `${parts.slice(0, 2).join(':')}…${parts[parts.length - 1]}`;
136
+ }
137
+
138
+ function fmtDate(d) {
139
+ return d instanceof Date && !isNaN(d) ? d.toISOString().slice(0, 10) : 'unknown';
140
+ }
141
+
142
+ // ── the checks ─────────────────────────────────────────────────────────────────────────────────
143
+
144
+ const pass = (key, text) => ({ key, status: 'pass', text });
145
+ const fail = (key, text) => ({ key, status: 'fail', text });
146
+ const skip = (key, text) => ({ key, status: 'skip', text });
147
+
148
+ /**
149
+ * Run the four (occasionally five) checks against a supervisor.
150
+ *
151
+ * @param {object} o
152
+ * instance the id this stack must report as itself
153
+ * statusUrl the status listener
154
+ * statusToken bearer for the status document
155
+ * caCert the certificate to pin, PEM
156
+ * enginesKey bearer for inference — checked only when there is an engine to check it against
157
+ * engineName an adopted engine to authenticate against; omitted, the first healthy one is used
158
+ * engineId its stable id, when the row has one — matched instead of the name (renames)
159
+ * fetchImpl test seam; the pinned transport is used when absent
160
+ */
161
+ async function verify(o = {}) {
162
+ const instance = String(o.instance || '');
163
+ const statusUrl = String(o.statusUrl || '');
164
+ const timeoutMs = Number(o.timeoutMs) || DEFAULT_TIMEOUT_MS;
165
+ const checks = [];
166
+ const at = new Date().toISOString();
167
+
168
+ const pin = readPin(o.caCert);
169
+ const host = hostOf(statusUrl);
170
+
171
+ // ── 1 + 2 + 3: one request answers all three, because they fail at different layers of it ──
172
+ let doc = null;
173
+ let found = null; // the engine list, when the document arrived
174
+ try {
175
+ const started = Date.now();
176
+ const res = await statusFetch(o, statusUrl, timeoutMs);
177
+ const ms = Date.now() - started;
178
+
179
+ checks.push(pass('reachable', `reached ${host} in ${ms} ms`));
180
+ checks.push(certCheck(pin, true));
181
+
182
+ if (res.status === 401 || res.status === 403) {
183
+ checks.push(fail('status_token',
184
+ `status token rejected — the listener answered ${res.status}`));
185
+ checks.push(skip('engines_key', 'engines key not checked — needs the engine list'));
186
+ return finish(instance, at, checks, null, found);
187
+ }
188
+ if (!res.ok) {
189
+ checks.push(fail('status_token', `the listener answered ${res.status}`));
190
+ checks.push(skip('engines_key', 'engines key not checked — needs the engine list'));
191
+ return finish(instance, at, checks, null, found);
192
+ }
193
+
194
+ try {
195
+ doc = await res.json();
196
+ } catch (e) {
197
+ checks.push(fail('status_token', 'the listener answered, but not with a status document'));
198
+ checks.push(skip('engines_key', 'engines key not checked — needs the engine list'));
199
+ return finish(instance, at, checks, null, found);
200
+ }
201
+
202
+ const engines = Array.isArray(doc && doc.engines) ? doc.engines : [];
203
+ checks.push(pass('status_token',
204
+ `status token accepted · ${engines.length} engine${engines.length === 1 ? '' : 's'} reported`));
205
+
206
+ // THE DOCUMENT COMES BACK WITH THE VERDICT. Verifying already costs a status read, and that read
207
+ // is the same one the engine picker used to make on demand — so carrying the list here lets the
208
+ // panel render its engines from what we just fetched instead of asking again from the browser.
209
+ // It is also what removes the "Discover engines" button: a list you have to press for is a list
210
+ // that is stale by definition, and pressing it was where the 401 used to hide.
211
+ found = engines;
212
+
213
+ // IDENTITY, not connectivity. Engine names collide across deployments — `analysis` exists on
214
+ // every stack — so a status URL pointed at the wrong machine answers plausibly and sends work
215
+ // somewhere else. Only raised when it is actually wrong; a matching id needs no line.
216
+ if (doc && doc.instance && String(doc.instance) !== instance) {
217
+ checks.push(fail('instance',
218
+ `this stack reports itself as “${doc.instance}”, not “${instance}” — the address points at a different deployment`));
219
+ // AND ITS ENGINES ARE NOT HANDED BACK (0.25.0). The apps' scheduled refresh feeds
220
+ // report.engines to rekeyPlan; returning another deployment's list wrote THAT stack's engine
221
+ // ids onto this stack's rows — irreversibly, since a row with an id is never re-pointed.
222
+ // `null` is "no document for this stack", which every consumer treats as "write nothing".
223
+ found = null;
224
+ }
225
+
226
+ checks.push(await enginesKeyCheck(o, engines, timeoutMs));
227
+ return finish(instance, at, checks, null, found);
228
+ } catch (err) {
229
+ const { kind, code } = classify(err);
230
+ if (kind === 'tls') {
231
+ // NOTHING WAS SENT. The handshake failed, so neither credential left this process — worth
232
+ // saying, because it means the tokens are not what to go and look at.
233
+ checks.push(pass('reachable', `reached ${host}`));
234
+ checks.push(fail('certificate', certFailText(pin, code)));
235
+ checks.push(skip('status_token', 'status token not sent — the connection was refused first'));
236
+ checks.push(skip('engines_key', 'engines key not sent'));
237
+ return finish(instance, at, checks, null, found);
238
+ }
239
+ checks.push(fail('reachable', `no answer from ${host} (${code})`));
240
+ checks.push(skip('certificate', 'certificate not checked'));
241
+ checks.push(skip('status_token', 'status token not checked'));
242
+ checks.push(skip('engines_key', 'engines key not checked'));
243
+ return finish(instance, at, checks, pin, found);
244
+ }
245
+ }
246
+
247
+ function hostOf(url) {
248
+ try { const u = new URL(url); return u.host; } catch (e) { return url || 'the listener'; }
249
+ }
250
+
251
+ /** The status read, over the pinned transport unless a test supplies its own fetch. */
252
+ async function statusFetch(o, statusUrl, timeoutMs) {
253
+ const headers = { Accept: 'application/json' };
254
+ if (o.statusToken) headers.Authorization = `Bearer ${o.statusToken}`;
255
+ return send(o, statusUrl, headers, timeoutMs);
256
+ }
257
+
258
+ /**
259
+ * One request, over the pinned transport.
260
+ *
261
+ * THE DISPATCHER IS HALF THE TRANSPORT. `lmxTransport(ca)` returns `{ fetch, dispatcher }` and the
262
+ * certificate lives on the DISPATCHER — taking only `.fetch` and calling it leaves the pin behind,
263
+ * and the request goes out against the system trust store instead. Against a self-signed stack that
264
+ * surfaces as DEPTH_ZERO_SELF_SIGNED_CERT, which reads exactly like a genuine certificate mismatch:
265
+ * a verifier that reported "the certificate did not match" for a perfectly good certificate, which
266
+ * is a worse failure than the one it was written to catch.
267
+ */
268
+ async function send(o, url, headers, timeoutMs) {
269
+ const opts = { headers, signal: AbortSignal.timeout(timeoutMs) };
270
+ if (o.fetchImpl) return o.fetchImpl(url, opts);
271
+ const { lmxTransport } = require('./providers/lmxTransport');
272
+ const t = lmxTransport(o.caCert || null);
273
+ if (t.dispatcher) opts.dispatcher = t.dispatcher;
274
+ return t.fetch(url, opts);
275
+ }
276
+
277
+ function certCheck(pin, handshakeOk) {
278
+ if (!pin.present) {
279
+ // Not a failure: a stack on loopback, or one behind a certificate the system already trusts,
280
+ // needs no pin. Saying "none pinned" is the true statement; a tick would claim a check ran.
281
+ return skip('certificate', 'no certificate pinned — the system trust store was used');
282
+ }
283
+ if (!pin.valid) return fail('certificate', `the pinned certificate could not be read (${pin.error})`);
284
+ if (pin.expired) {
285
+ return fail('certificate',
286
+ `the pinned certificate EXPIRED on ${fmtDate(pin.expires)} — every call fails at the handshake`);
287
+ }
288
+ const base = `certificate matched the pin · ${pin.fingerprint} · expires ${fmtDate(pin.expires)}`;
289
+ if (pin.expiringSoon) {
290
+ return { key: 'certificate', status: 'warn', text: `${base} — ${pin.expiresDays} days left` };
291
+ }
292
+ return handshakeOk ? pass('certificate', base) : skip('certificate', base);
293
+ }
294
+
295
+ function certFailText(pin, code) {
296
+ if (pin.present && pin.expired) {
297
+ return `the pinned certificate expired on ${fmtDate(pin.expires)} (${code})`;
298
+ }
299
+ if (code === 'CERT_HAS_EXPIRED') return 'the stack is presenting an expired certificate';
300
+ if (code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
301
+ return 'the certificate is not valid for this address — check the hostname';
302
+ }
303
+ if (!pin.present) {
304
+ return 'the stack presented a certificate this machine does not trust, and none is pinned here';
305
+ }
306
+ return `the stack presented a different certificate from the one pinned here (${code})`;
307
+ }
308
+
309
+ /**
310
+ * Check 4 — WITHOUT SPENDING ANYTHING.
311
+ *
312
+ * The engines key is a bearer on the engine's own OpenAI-compatible API, so asking that API to list
313
+ * its models authenticates against exactly the route inference uses. A completion would prove the
314
+ * same thing and bill for the privilege.
315
+ */
316
+ async function enginesKeyCheck(o, engines, timeoutMs) {
317
+ if (!o.enginesKey) {
318
+ return skip('engines_key', 'no engines key set — engines are being called unauthenticated');
319
+ }
320
+ const { findEngine } = require('./providers/lmxDiscovery');
321
+ const wanted = (o.engineId || o.engineName)
322
+ ? findEngine(engines, { id: o.engineId || null, name: o.engineName })
323
+ : engines.find((e) => e && e.state === 'healthy' && e.url);
324
+ if (!wanted || !wanted.url) {
325
+ return skip('engines_key',
326
+ o.engineName
327
+ ? `engines key not checked — “${o.engineName}” is not currently healthy`
328
+ : 'engines key not checked — no healthy engine to authenticate against');
329
+ }
330
+ try {
331
+ const url = `${String(wanted.url).replace(/\/+$/, '')}/models`;
332
+ const res = await send(o, url,
333
+ { Accept: 'application/json', Authorization: `Bearer ${o.enginesKey}` }, timeoutMs);
334
+ if (res.status === 401 || res.status === 403) {
335
+ return fail('engines_key',
336
+ `engines key rejected by ${wanted.name} — inference calls will fail with ${res.status}`);
337
+ }
338
+ if (!res.ok) {
339
+ return skip('engines_key', `engines key not confirmed — ${wanted.name} answered ${res.status}`);
340
+ }
341
+ return pass('engines_key', `engines key accepted · checked against ${wanted.name}`);
342
+ } catch (err) {
343
+ const { code } = classify(err);
344
+ return skip('engines_key', `engines key not checked — ${wanted.name} did not answer (${code})`);
345
+ }
346
+ }
347
+
348
+ // ── the verdict ────────────────────────────────────────────────────────────────────────────────
349
+
350
+ function finish(instance, at, checks, pinForOffline, engines) {
351
+ // A certificate that expires soon is worth saying even when nothing could be reached, because it
352
+ // is knowable from the PEM alone and is exactly the failure nobody sees coming.
353
+ if (pinForOffline && pinForOffline.present && pinForOffline.valid
354
+ && (pinForOffline.expired || pinForOffline.expiringSoon)) {
355
+ const i = checks.findIndex((c) => c.key === 'certificate');
356
+ if (i > -1) checks[i] = certCheck(pinForOffline, false);
357
+ }
358
+
359
+ const failed = checks.filter((c) => c.status === 'fail');
360
+ const warned = checks.filter((c) => c.status === 'warn');
361
+ const ok = failed.length === 0;
362
+
363
+ return {
364
+ ok,
365
+ level: failed.length ? (failed.some((c) => c.key === 'certificate') ? 'bad' : 'warn')
366
+ : (warned.length ? 'warn' : 'ok'),
367
+ headline: headlineFor(failed, warned),
368
+ at,
369
+ instance,
370
+ checks,
371
+ // Null means "we never got a document", which is NOT the same as a stack with no engines — the
372
+ // panel must be able to say "not checked" rather than "no engines", because those send an
373
+ // operator to entirely different places.
374
+ engines: engines || null
375
+ };
376
+ }
377
+
378
+ /**
379
+ * ONE SENTENCE NAMING THE CAUSE, for the flash. "Saved, but the status token is being rejected"
380
+ * is a different instruction from "saved, but the stack did not answer" — the first sends somebody
381
+ * to a credential, the second to a machine, and "401" sent them to neither.
382
+ */
383
+ function headlineFor(failed, warned) {
384
+ if (!failed.length) {
385
+ return warned.length ? warned[0].text : 'connected, certificate pinned, both credentials accepted';
386
+ }
387
+ const first = failed[0];
388
+ switch (first.key) {
389
+ case 'reachable': return 'the stack did not answer — its settings are stored and will be used when it is back';
390
+ case 'certificate': return 'the certificate did not match — nothing was sent';
391
+ case 'status_token': return 'the status token is being rejected — engines cannot be listed until it is corrected';
392
+ case 'instance': return 'this address points at a different deployment';
393
+ case 'engines_key': return 'the engines key is being rejected — inference calls will fail';
394
+ default: return first.text;
395
+ }
396
+ }
397
+
398
+ module.exports = {
399
+ verify, remember, lastFor, forget, readPin,
400
+ DEFAULT_TIMEOUT_MS, EXPIRY_WARN_DAYS, REPORT_TTL_MS,
401
+ _reset, _classify: classify, _shortPrint: shortPrint
402
+ };