import { randomUUID } from "node:crypto" ;
import { formatErrorMessage } from "../infra/errors.js" ;
import type { SystemPresence } from "../infra/system-presence.js" ;
import { MAX_SAFE_TIMEOUT_DELAY_MS, resolveSafeTimeoutDelayMs } from "../utils/timer-delay.js" ;
import { GatewayClient, GatewayClientRequestError } from "./client.js" ;
import { READ_SCOPE } from "./method-scopes.js" ;
import { isLoopbackHost } from "./net.js" ;
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "./protocol/client-info.js" ;
export type GatewayProbeAuth = {
token?: string;
password?: string;
};
export type GatewayProbeClose = {
code: number;
reason: string;
hint?: string;
};
export type GatewayProbeCapability =
| "unknown"
| "pairing_pending"
| "connected_no_operator_scope"
| "read_only"
| "write_capable"
| "admin_capable" ;
export type GatewayProbeAuthSummary = {
role: string | null ;
scopes: string[];
capability: GatewayProbeCapability;
};
export type GatewayProbeResult = {
ok: boolean ;
url: string;
connectLatencyMs: number | null ;
error: string | null ;
connectErrorDetails?: unknown;
close: GatewayProbeClose | null ;
auth: GatewayProbeAuthSummary;
health: unknown;
status: unknown;
presence: SystemPresence[] | null ;
configSnapshot: unknown;
};
export const MIN_PROBE_TIMEOUT_MS = 250 ;
export const MAX_TIMER_DELAY_MS = MAX_SAFE_TIMEOUT_DELAY_MS;
const PAIRING_REQUIRED_PATTERN = /\bpairing required\b/i;
const OPERATOR_READ_SCOPE = "operator.read" ;
const OPERATOR_WRITE_SCOPE = "operator.write" ;
const OPERATOR_ADMIN_SCOPE = "operator.admin" ;
export function clampProbeTimeoutMs(timeoutMs: number): number {
return resolveSafeTimeoutDelayMs(timeoutMs, { minMs: MIN_PROBE_TIMEOUT_MS });
}
function formatProbeCloseError(close: GatewayProbeClose): string {
return `gateway closed (${close.code}): ${close.reason}`;
}
function emptyProbeAuth(): GatewayProbeAuthSummary {
return {
role: null ,
scopes: [],
capability: "unknown" ,
};
}
function resolveProbeAuthSummary(params: {
role?: string | null ;
scopes?: string[];
authMetadataPresent?: boolean ;
error?: string | null ;
close?: GatewayProbeClose | null ;
verifiedRead?: boolean ;
connectLatencyMs?: number | null ;
}): GatewayProbeAuthSummary {
const scopes = Array.isArray(params.scopes) ? params.scopes : [];
return {
role: params.role ?? null ,
scopes,
capability: resolveGatewayProbeCapability({
auth: { scopes },
authMetadataPresent: params.authMetadataPresent,
error: params.error,
close: params.close,
verifiedRead: params.verifiedRead,
connectLatencyMs: params.connectLatencyMs,
}),
};
}
export function isPairingPendingProbeFailure(params: {
error?: string | null ;
close?: GatewayProbeClose | null ;
}): boolean {
return PAIRING_REQUIRED_PATTERN.test(params.close?.reason ?? params.error ?? "" );
}
export function resolveGatewayProbeCapability(params: {
auth?: Pick<GatewayProbeAuthSummary, "scopes" > | null ;
authMetadataPresent?: boolean ;
error?: string | null ;
close?: GatewayProbeClose | null ;
verifiedRead?: boolean ;
connectLatencyMs?: number | null ;
}): GatewayProbeCapability {
if (isPairingPendingProbeFailure(params)) {
return "pairing_pending" ;
}
const scopes = Array.isArray(params.auth?.scopes) ? params.auth.scopes : [];
if (scopes.includes(OPERATOR_ADMIN_SCOPE)) {
return "admin_capable" ;
}
if (scopes.includes(OPERATOR_WRITE_SCOPE)) {
return "write_capable" ;
}
if (scopes.includes(OPERATOR_READ_SCOPE) || params.verifiedRead === true ) {
return "read_only" ;
}
if (params.connectLatencyMs != null && params.authMetadataPresent === true ) {
return "connected_no_operator_scope" ;
}
return "unknown" ;
}
export async function probeGateway(opts: {
url: string;
auth?: GatewayProbeAuth;
timeoutMs: number;
includeDetails?: boolean ;
detailLevel?: "none" | "presence" | "full" ;
tlsFingerprint?: string;
}): Promise<GatewayProbeResult> {
const startedAt = Date.now();
const instanceId = randomUUID();
let connectLatencyMs: number | null = null ;
let connectError: string | null = null ;
let connectErrorDetails: unknown = null ;
let close: GatewayProbeClose | null = null ;
let auth = emptyProbeAuth();
let authMetadataPresent = false ;
const detailLevel = opts.includeDetails === false ? "none" : (opts.detailLevel ?? "full" );
const deviceIdentity = await (async () => {
let hostname: string;
try {
hostname = new URL(opts.url).hostname;
} catch {
return null ;
}
// Local authenticated probes should stay device-bound so read/detail RPCs
// are not scope-limited by the shared-auth scope stripping hardening.
if (isLoopbackHost(hostname) && !(opts.auth?.token || opts.auth?.password)) {
return null ;
}
try {
const { loadOrCreateDeviceIdentity } = await import ("../infra/device-identity.js" );
return loadOrCreateDeviceIdentity();
} catch {
// Read-only or restricted environments should still be able to run
// token/password-auth detail probes without crashing on identity persistence.
return null ;
}
})();
return await new Promise<GatewayProbeResult>((resolve) => {
let settled = false ;
let timer: ReturnType<typeof setTimeout> | null = null ;
const clearProbeTimer = () => {
if (timer) {
clearTimeout(timer);
timer = null ;
}
};
const armProbeTimer = (onTimeout: () => void ) => {
clearProbeTimer();
timer = setTimeout(onTimeout, clampProbeTimeoutMs(opts.timeoutMs));
};
const settle = (
result: Omit<GatewayProbeResult, "url" | "connectErrorDetails" > & {
connectErrorDetails?: unknown;
},
) => {
if (settled) {
return ;
}
settled = true ;
clearProbeTimer();
client.stop();
const { connectErrorDetails: resultConnectErrorDetails, ...rest } = result;
resolve({
url: opts.url,
...rest,
...(resultConnectErrorDetails != null
? { connectErrorDetails: resultConnectErrorDetails }
: {}),
});
};
const settleProbe = (params: {
ok: boolean ;
error: string | null ;
verifiedRead?: boolean ;
health: unknown;
status: unknown;
presence: SystemPresence[] | null ;
configSnapshot: unknown;
}) => {
settle({
ok: params.ok,
connectLatencyMs,
error: params.error,
connectErrorDetails,
close,
auth: resolveProbeAuthSummary({
role: auth.role,
scopes: auth.scopes,
authMetadataPresent,
error: params.error,
close,
verifiedRead: params.verifiedRead,
connectLatencyMs,
}),
health: params.health,
status: params.status,
presence: params.presence,
configSnapshot: params.configSnapshot,
});
};
const client = new GatewayClient({
url: opts.url,
token: opts.auth?.token,
password: opts.auth?.password,
tlsFingerprint: opts.tlsFingerprint,
scopes: [READ_SCOPE],
clientName: GATEWAY_CLIENT_NAMES.CLI,
clientVersion: "dev" ,
mode: GATEWAY_CLIENT_MODES.PROBE,
instanceId,
deviceIdentity,
onConnectError: (err) => {
connectError = formatErrorMessage(err);
connectErrorDetails = err instanceof GatewayClientRequestError ? err.details : null ;
},
onClose: (code, reason) => {
close = { code, reason };
if (connectLatencyMs == null ) {
settleProbe({
ok: false ,
error: connectError || formatProbeCloseError(close),
health: null ,
status: null ,
presence: null ,
configSnapshot: null ,
});
}
},
onHelloOk: async (hello) => {
connectLatencyMs = Date.now() - startedAt;
authMetadataPresent = typeof hello?.auth === "object" && hello.auth !== null ;
auth = resolveProbeAuthSummary({
role: typeof hello?.auth?.role === "string" ? hello.auth.role : null ,
scopes: Array.isArray(hello?.auth?.scopes)
? hello.auth.scopes.filter((scope): scope is string => typeof scope === "string" )
: [],
authMetadataPresent,
});
if (detailLevel === "none" ) {
settleProbe({
ok: true ,
error: null ,
verifiedRead: false ,
health: null ,
status: null ,
presence: null ,
configSnapshot: null ,
});
return ;
}
// Once the gateway has accepted the session, a slow follow-up RPC should no longer
// downgrade the probe to "unreachable". Give detail fetching its own budget.
armProbeTimer(() => {
settleProbe({
ok: false ,
error: "timeout" ,
health: null ,
status: null ,
presence: null ,
configSnapshot: null ,
});
});
try {
if (detailLevel === "presence" ) {
const presence = await client.request("system-presence" );
settleProbe({
ok: true ,
error: null ,
verifiedRead: true ,
health: null ,
status: null ,
presence: Array.isArray(presence) ? (presence as SystemPresence[]) : null ,
configSnapshot: null ,
});
return ;
}
const [health, status, presence, configSnapshot] = await Promise.all([
client.request("health" ),
client.request("status" ),
client.request("system-presence" ),
client.request("config.get" , {}),
]);
settleProbe({
ok: true ,
error: null ,
verifiedRead: true ,
health,
status,
presence: Array.isArray(presence) ? (presence as SystemPresence[]) : null ,
configSnapshot,
});
} catch (err) {
const error = formatErrorMessage(err);
settleProbe({
ok: false ,
error,
health: null ,
status: null ,
presence: null ,
configSnapshot: null ,
});
}
},
});
armProbeTimer(() => {
const error = connectError ? `connect failed: ${connectError}` : "timeout" ;
settleProbe({
ok: false ,
error,
health: null ,
status: null ,
presence: null ,
configSnapshot: null ,
});
});
client.start();
});
}
Messung V0.5 in Prozent C=99 H=100 G=99
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland