fix: harden workspace diagnostic protocols

This commit is contained in:
2026-08-03 23:59:46 +02:00
parent 9e2eafb66c
commit ca97bbb9c2
5 changed files with 371 additions and 17 deletions
+150 -2
View File
@@ -7,7 +7,9 @@
"name": "thothii-backend",
"dependencies": {
"@fastify/cors": "^11.2.0",
"@types/pg": "^8.20.3",
"fastify": "^5.0.0",
"pg": "^8.22.0",
"yaml": "^2.9.0",
"yauzl": "^3.4.0",
"yazl": "^3.3.1",
@@ -968,12 +970,22 @@
"version": "22.20.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz",
"integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/pg": {
"version": "8.20.3",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.3.tgz",
"integrity": "sha512-4Tvg+HO6+oQaAkpT8GTYoSExzpGGZz532GXgbbCElWJQeQdMozBWxEKNBhJJpHFjWXsMxqPbyypvj/89FWNoSQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^2.2.0"
}
},
"node_modules/@types/yauzl": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-3.4.0.tgz",
@@ -1634,6 +1646,95 @@
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"license": "MIT"
},
"node_modules/pg": {
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
"integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.14.0",
"pg-pool": "^3.14.0",
"pg-protocol": "^1.15.0",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.4.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
"integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz",
"integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1707,6 +1808,45 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
@@ -2039,7 +2179,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {
@@ -2638,6 +2777,15 @@
"node": ">=8"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yaml": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
+2
View File
@@ -10,7 +10,9 @@
},
"dependencies": {
"@fastify/cors": "^11.2.0",
"@types/pg": "^8.20.3",
"fastify": "^5.0.0",
"pg": "^8.22.0",
"yaml": "^2.9.0",
"yauzl": "^3.4.0",
"yazl": "^3.3.1",
+136 -14
View File
@@ -1,7 +1,8 @@
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { readFile, realpath } from "node:fs/promises";
import { createConnection } from "node:net";
import { once } from "node:events";
import { Client } from "pg";
import { MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS } from "../config.js";
import { buildInstallationContract } from "./contracts.js";
import type { RuntimeBindings } from "./runtime-renderer.js";
@@ -90,6 +91,10 @@ export interface VectorDiagnosticRequest {
collection: string;
dimensions?: number;
distance?: "cosine" | "l2" | "inner_product";
host?: string;
port?: number;
user?: string;
resource?: DiagnosticResource;
timeoutMs: number;
signal: AbortSignal;
}
@@ -130,6 +135,17 @@ export interface DirectProtocolFactory {
probe(request: ConnectorDiagnosticRequest): Promise<ConnectorDiagnosticResult>;
}
export interface DatabaseDiagnosticClient {
query(sql: string, values: readonly unknown[]): Promise<{ rows: Array<Record<string, unknown>> }>;
end(): Promise<void>;
}
export interface DatabaseDiagnosticClientFactory {
connect(request: {
host: string; port: number; database: string; user: string; credentialFile: string; tlsCaFile: string; signal: AbortSignal;
}): Promise<DatabaseDiagnosticClient>;
}
export interface SshProcessFactory {
start(request: SshTunnelRequest, args: readonly string[]): Promise<{
tunnel: LoopbackTunnel;
@@ -140,6 +156,7 @@ export interface SshProcessFactory {
export interface ConcreteDiagnosticAdapterDependencies {
directProtocol?: DirectProtocolFactory;
sshProcess?: SshProcessFactory;
databaseClient?: DatabaseDiagnosticClientFactory;
}
/**
@@ -176,6 +193,24 @@ async function secretPresent(file: string): Promise<boolean> {
return (await readFile(file, "utf8")).trim().length > 0;
}
async function sameSecretFile(first: string, second: string): Promise<boolean> {
try {
return await realpath(first) === await realpath(second);
} catch {
return first === second;
}
}
async function restHeaders(
diagnostic: RestDiagnosticRequest,
credentialFile: string | undefined,
): Promise<Record<string, string>> {
if (diagnostic.auth === "none") return {};
if (!credentialFile || !(await secretPresent(credentialFile))) throw new Error("REST probe failed");
const secret = (await readFile(credentialFile, "utf8")).trim();
return diagnostic.auth === "bearer" ? { authorization: `Bearer ${secret}` } : { "x-api-key": secret };
}
/**
* Concrete production adapters deliberately retain only probe metadata. Protocol failures and
* response bodies are discarded at this boundary; callers receive fixed diagnostics instead.
@@ -183,23 +218,57 @@ async function secretPresent(file: string): Promise<boolean> {
export function createConcreteDiagnosticAdapters(
dependencies: ConcreteDiagnosticAdapterDependencies = {},
): DiagnosticAdapters {
const databaseClient = dependencies.databaseClient ?? {
async connect(request: { host: string; port: number; database: string; user: string; credentialFile: string; tlsCaFile: string; signal: AbortSignal }) {
const client = new Client({
host: request.host, port: request.port, database: request.database, user: request.user,
password: (await readFile(request.credentialFile, "utf8")).trim(),
ssl: { ca: await readFile(request.tlsCaFile, "utf8"), rejectUnauthorized: true },
connectionTimeoutMillis: 5_000,
});
const abort = () => { void client.end(); };
request.signal.addEventListener("abort", abort, { once: true });
try {
await client.connect();
return { query: async (sql: string, values: readonly unknown[]) => await client.query(sql, [...values]), end: async () => { request.signal.removeEventListener("abort", abort); await client.end(); } };
} catch (error) {
request.signal.removeEventListener("abort", abort);
await client.end().catch(() => undefined);
throw error;
}
},
};
const directProtocol = dependencies.directProtocol ?? {
async probe(request: ConnectorDiagnosticRequest): Promise<ConnectorDiagnosticResult> {
if (!request.host || !request.port || !request.credentialFile || !(await secretPresent(request.credentialFile))) {
if (!request.host || !request.port || !request.user || !request.credentialFile || !request.tlsCaFile
|| !(await secretPresent(request.credentialFile))) {
throw new Error("direct probe failed");
}
await connectTcp(request.host, request.port, request.signal);
return { resolved: true, tlsVerified: request.tlsCaFile !== undefined, authenticated: true, resource: request.resource };
const database = request.resource.database;
const schema = request.resource.schema;
if (!database || !schema) throw new Error("direct probe failed");
const client = await databaseClient.connect({
host: request.host, port: request.port, database, user: request.user,
credentialFile: request.credentialFile, tlsCaFile: request.tlsCaFile, signal: request.signal,
});
try {
const result = await client.query("SELECT current_database() AS database, current_schema() AS schema", []);
const row = result.rows[0];
if (row?.database !== database || row.schema !== schema) throw new Error("direct probe failed");
return { resolved: true, tlsVerified: true, authenticated: true, resource: request.resource };
} finally {
await client.end().catch(() => undefined);
}
},
};
return {
async probeConnector(request) {
if (request.transport === "rest_api") {
if (!request.baseUrl || !request.diagnostic || !request.credentialFile || !(await secretPresent(request.credentialFile))) throw new Error("REST probe failed");
if (!request.baseUrl || !request.diagnostic || request.tlsCaFile) throw new Error("REST probe failed");
const endpoint = resolveDiagnosticUrl(request.baseUrl, request.diagnostic.path);
const response = await fetch(endpoint.toString(), {
method: request.diagnostic.method,
headers: { authorization: `Bearer ${(await readFile(request.credentialFile, "utf8")).trim()}` },
headers: await restHeaders(request.diagnostic, request.credentialFile),
signal: request.signal,
redirect: "error",
});
@@ -241,6 +310,28 @@ export function createConcreteDiagnosticAdapters(
}
},
async inspectVector(request) {
if (request.transport === "pgvector_direct" || request.transport === "ssh_tunnel") {
const resource = request.resource;
if (!request.host || !request.port || !request.user || !request.credentialFile || !request.tlsCaFile
|| !resource?.database || !resource.schema || !(await secretPresent(request.credentialFile))) {
throw new Error("vector metadata adapter is unavailable");
}
const client = await databaseClient.connect({
host: request.host, port: request.port, database: resource.database, user: request.user,
credentialFile: request.credentialFile, tlsCaFile: request.tlsCaFile, signal: request.signal,
});
try {
const metadata = await client.query(
"SELECT a.atttypmod - 4 AS dimensions, CASE WHEN pg_get_indexdef(i.indexrelid) LIKE '%vector_cosine_ops%' THEN 'cosine' WHEN pg_get_indexdef(i.indexrelid) LIKE '%vector_l2_ops%' THEN 'l2' WHEN pg_get_indexdef(i.indexrelid) LIKE '%vector_ip_ops%' THEN 'inner_product' END AS distance FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid JOIN pg_namespace n ON n.oid = c.relnamespace LEFT JOIN pg_index i ON i.indrelid = c.oid WHERE n.nspname = $1 AND c.relname = $2 AND a.attnum > 0 AND NOT a.attisdropped AND a.atttypid = (SELECT oid FROM pg_type WHERE typname = 'vector') LIMIT 1",
[resource.schema, request.collection],
);
const row = metadata.rows[0];
if (!row || !Number.isInteger(row.dimensions) || (row.distance !== "cosine" && row.distance !== "l2" && row.distance !== "inner_product")) throw new Error("vector metadata adapter is unavailable");
return { collection: request.collection, dimensions: row.dimensions as number, distance: row.distance as VectorDiagnosticResult["distance"] };
} finally {
await client.end().catch(() => undefined);
}
}
if (request.transport !== "rest_api" || !request.baseUrl || !request.credentialFile || !request.diagnostic
|| !(await secretPresent(request.credentialFile))) throw new Error("vector metadata adapter is unavailable");
const response = await fetch(resolveDiagnosticUrl(request.baseUrl, request.diagnostic.path).toString(), {
@@ -514,6 +605,7 @@ export function createWorkspaceDiagnoser(
const dwhTimeout = boundedTimeout(canonical.dwh.timeout_ms, fallbackTimeout);
const vectorTimeout = boundedTimeout(canonical.semantic_index.vector_store.timeout_ms, fallbackTimeout);
const embeddingTimeout = boundedTimeout(canonical.semantic_index.embedding.timeout_ms, fallbackTimeout);
let tunneledVectorMetadata: VectorDiagnosticResult | undefined;
for (const role of ["dwh", "vector"] as const) {
const timeoutMs = role === "dwh" ? dwhTimeout : vectorTimeout;
@@ -526,9 +618,22 @@ export function createWorkspaceDiagnoser(
const result = "sshHost" in request
? await withTimeout(timeoutMs, (signal) => adapters.withSshTunnel(
{ ...request, signal },
(tunnel) => adapters.probeConnector(tunnelProbeRequest(
canonical, role, bindings, timeoutMs, tunnel, signal,
)),
async (tunnel) => {
const tunneledRequest = tunnelProbeRequest(canonical, role, bindings, timeoutMs, tunnel, signal);
const connector = await adapters.probeConnector(tunneledRequest);
if (role === "vector") {
tunneledVectorMetadata = await adapters.inspectVector({
transport: "ssh_tunnel", host: tunneledRequest.host, port: tunneledRequest.port,
user: tunneledRequest.user, credentialFile: tunneledRequest.credentialFile,
tlsCaFile: tunneledRequest.tlsCaFile, resource: tunneledRequest.resource,
collection: canonical.semantic_index.vector_store.collection,
dimensions: canonical.semantic_index.vector_store.dimensions,
distance: canonical.semantic_index.vector_store.distance,
timeoutMs: vectorTimeout, signal,
});
}
return connector;
},
))
: await withTimeout(timeoutMs, (signal) => adapters.probeConnector({ ...request, signal }));
const resource = role === "dwh"
@@ -549,7 +654,9 @@ export function createWorkspaceDiagnoser(
try {
const vectorBinding = bindings.vector;
const vectorRest = canonical.diagnostics?.vector_rest?.metadata;
const vector = await withTimeout(vectorTimeout, (signal) => adapters.inspectVector({
const vectorDirect = connectorRequest(canonical, "vector", bindings, vectorTimeout);
const directVectorRequest = vectorDirect && !("sshHost" in vectorDirect) ? vectorDirect : undefined;
const vector = tunneledVectorMetadata ?? await withTimeout(vectorTimeout, (signal) => adapters.inspectVector({
transport: vectorBinding.transport === "pgvector_direct" || vectorBinding.transport === "rest_api"
|| vectorBinding.transport === "ssh_tunnel" ? vectorBinding.transport : undefined,
baseUrl: vectorBinding.values[bindingName(canonical, "VECTOR", "BASE_URL")],
@@ -559,6 +666,14 @@ export function createWorkspaceDiagnoser(
collection: canonical.semantic_index.vector_store.collection,
dimensions: canonical.semantic_index.vector_store.dimensions,
distance: canonical.semantic_index.vector_store.distance,
...(directVectorRequest ? {
host: directVectorRequest.host,
port: directVectorRequest.port,
user: directVectorRequest.user,
credentialFile: directVectorRequest.credentialFile,
tlsCaFile: directVectorRequest.tlsCaFile,
resource: directVectorRequest.resource,
} : {}),
timeoutMs: vectorTimeout,
signal,
}));
@@ -601,6 +716,11 @@ export function createWorkspaceDiagnoser(
) {
const credentialFile = bindings.vector.values[bindingName(canonical, "VECTOR_WRITER", "API_KEY_FILE")];
if (!credentialFile) return { activatable: true, diagnostics };
const readerCredentialFile = bindings.vector.values[bindingName(canonical, "VECTOR", "API_KEY_FILE")];
if (readerCredentialFile && await sameSecretFile(credentialFile, readerCredentialFile)) {
diagnostics.push(diagnosticError("binding_missing", bindingName(canonical, "VECTOR_WRITER", "API_KEY_FILE")));
return { activatable: false, diagnostics };
}
const request: WriteDiagnosticRecordRequest = {
collection: canonical.semantic_index.vector_store.collection,
id: `diagnostic:${randomUUID()}`,
@@ -611,20 +731,22 @@ export function createWorkspaceDiagnoser(
baseUrl: bindings.vector.values[bindingName(canonical, "VECTOR", "BASE_URL")],
diagnostic: canonical.diagnostics.vector_rest.reversible_probe,
};
let writeSucceeded = false;
let writeStarted = false;
let cleanupAttempted = false;
let cleanupFailed = false;
try {
writeStarted = true;
await withTimeout(vectorTimeout, (signal) => adapters.writeDiagnosticRecord({ ...request, signal }));
writeSucceeded = true;
cleanupAttempted = true;
await withTimeout(vectorTimeout, (signal) => adapters.removeDiagnosticRecord({ ...request, signal }));
} catch {
cleanupFailed = true;
} finally {
if (writeSucceeded && cleanupFailed) {
if (writeStarted && (!cleanupAttempted || cleanupFailed)) {
try {
await withTimeout(vectorTimeout, (signal) => adapters.removeDiagnosticRecord({ ...request, signal }));
} catch {
// The cleanup attempt is deliberately best-effort and remains redacted.
cleanupFailed = true;
}
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ export type VectorTransport = (typeof VECTOR_TRANSPORTS)[number];
export const REST_DIAGNOSTIC_METHODS = ["GET", "POST"] as const;
export type RestDiagnosticMethod = (typeof REST_DIAGNOSTIC_METHODS)[number];
export const DIAGNOSTIC_AUTH_MODES = ["none", "bearer"] as const;
export const DIAGNOSTIC_AUTH_MODES = ["none", "bearer", "x-api-key"] as const;
export type DiagnosticAuthMode = (typeof DIAGNOSTIC_AUTH_MODES)[number];
export interface RestDiagnosticRequest {
@@ -366,6 +366,31 @@ test("does not substitute the reader credential for a declared vector writer", a
expect(adapters.writeDiagnosticRecord).not.toHaveBeenCalled();
});
test("rejects a writer credential that aliases the reader credential", async () => {
const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-"));
const readerKey = join(directory, "reader-key");
const writerAlias = join(directory, "writer-key");
await writeFile(readerKey, "same-secret\n", { mode: 0o600 });
await (await import("node:fs/promises")).symlink(readerKey, writerAlias);
const adapters = successfulAdapters();
const aliasedBindings: RuntimeBindings = {
...writerBindings,
vector: { ...writerBindings.vector, values: {
...writerBindings.vector.values,
THT_WS_PSD_CLINICAL_VECTOR_API_KEY_FILE: readerKey,
THT_WS_PSD_CLINICAL_VECTOR_WRITER_API_KEY_FILE: writerAlias,
} },
};
try {
const result = await diagnose(adapters)(writerWorkspace, aliasedBindings, { writeProbe: true });
expect(result.activatable).toBe(false);
expect(adapters.writeDiagnosticRecord).not.toHaveBeenCalled();
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("uses the declared POST DWH ping endpoint without exposing its local credential", async () => {
const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-"));
const credentialFile = join(directory, "dwh-api-key");
@@ -403,6 +428,50 @@ test("uses the declared POST DWH ping endpoint without exposing its local creden
}
});
test("requires an authenticated TLS database query before direct diagnostics succeed", async () => {
const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-"));
const passwordFile = join(directory, "password");
const caFile = join(directory, "ca.pem");
await Promise.all([writeFile(passwordFile, "password\n", { mode: 0o600 }), writeFile(caFile, "test-ca\n")]);
const query = vi.fn(async () => ({ rows: [{ database: "warehouse", schema: "datawarehouse" }] }));
const end = vi.fn(async () => undefined);
const connect = vi.fn(async () => ({ query, end }));
try {
const result = await createConcreteDiagnosticAdapters({ databaseClient: { connect } } as any).probeConnector({
role: "dwh", transport: "postgres_direct", host: "127.0.0.1", port: 5432, user: "reader",
credentialFile: passwordFile, tlsCaFile: caFile,
resource: { database: "warehouse", schema: "datawarehouse" }, timeoutMs: 5000,
signal: new AbortController().signal,
});
expect(connect).toHaveBeenCalledWith(expect.objectContaining({ database: "warehouse", tlsCaFile: caFile }));
expect(query).toHaveBeenCalledWith(expect.stringContaining("current_database"), []);
expect(end).toHaveBeenCalledOnce();
expect(result).toMatchObject({ authenticated: true, tlsVerified: true });
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("honors a declared unauthenticated REST diagnostic without reading a credential", async () => {
const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ database: "warehouse", schema: "datawarehouse" }), {
status: 200, headers: { "content-type": "application/json" },
}));
vi.stubGlobal("fetch", fetchSpy);
try {
await expect(createConcreteDiagnosticAdapters().probeConnector({
role: "dwh", transport: "rest_api", baseUrl: "https://dwh.example.test",
resource: { database: "warehouse", schema: "datawarehouse" }, timeoutMs: 5000,
signal: new AbortController().signal,
diagnostic: { method: "POST", path: "/rpc/ping", auth: "none", response: { database: "database", schema: "schema" } } as any,
})).resolves.toMatchObject({ authenticated: true });
expect(fetchSpy.mock.calls[0]?.[1]).not.toMatchObject({ headers: expect.objectContaining({ authorization: expect.anything() }) });
} finally {
vi.unstubAllGlobals();
}
});
test("constructs the production diagnoser with the configured timeout and injected adapters", async () => {
const adapters = successfulAdapters();
@@ -430,3 +499,16 @@ test("retries bounded cleanup after a write-probe removal times out", async () =
expect(result).toMatchObject({ activatable: false });
expect(JSON.stringify(result)).not.toContain("timeout");
});
test("attempts bounded cleanup when a timed-out write may already have created the record", async () => {
const adapters = successfulAdapters({
writeDiagnosticRecord: vi.fn(() => new Promise<void>(() => undefined)),
});
const diagnoseWithShortTimeout = createWorkspaceDiagnoser(adapters, { timeoutMs: 10 });
const result = await diagnoseWithShortTimeout(writerWorkspace, writerBindings, { writeProbe: true });
expect(adapters.writeDiagnosticRecord).toHaveBeenCalledOnce();
expect(adapters.removeDiagnosticRecord).toHaveBeenCalledOnce();
expect(result.activatable).toBe(false);
});