Move project and host queries into native MCP with host-owned OAuth/context, while retaining memory, skills, actions and publication plugins. Add nearby-model queries and explicit chat/task/learning model defaults. Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped session ownership, pending input edits and investigation communication. Disable local-provider discovery: Server /health allowed native vLLM to probe /v1/models on port 8000. A native before/after regression prevents recurrence. Validation: 170 focused Bun tests; service/plugin typechecks; native discovery, provider-discovery and input-inbox smoke checks; no paid model inference.
82 lines
2.7 KiB
TypeScript
82 lines
2.7 KiB
TypeScript
import { config } from "../config.js";
|
|
import type { components } from "../generated/server-api.js";
|
|
|
|
export type ProductCredential = {
|
|
authorization: string;
|
|
projectId: string;
|
|
expiresAt?: string | null;
|
|
};
|
|
|
|
export type AgentAuthContext = components["schemas"]["AgentAuthContextResponse"];
|
|
|
|
export class ProductServerClient {
|
|
constructor(private readonly baseUrl = config.TJWATER_SERVER_BASE_URL) {}
|
|
|
|
async authenticateUser(authorization: string): Promise<{ user_id: string }> {
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(new URL("/api/v1/access-context", this.baseUrl), {
|
|
headers: { Accept: "application/json", Authorization: authorization },
|
|
});
|
|
} catch { throw new ProductServerClientError(503, "TJWater Server unavailable"); }
|
|
const parsed = parseJson(await response.text());
|
|
if (!response.ok) throw new ProductServerClientError(response.status, "TJWater Server rejected identity verification");
|
|
if (!parsed || typeof parsed !== "object" || !("user_id" in parsed) || typeof parsed.user_id !== "string" || !parsed.user_id.trim()) {
|
|
throw new ProductServerClientError(502, "TJWater Server returned invalid identity");
|
|
}
|
|
return { user_id: parsed.user_id };
|
|
}
|
|
|
|
async authenticate(credential: ProductCredential): Promise<AgentAuthContext> {
|
|
const url = new URL("/api/v1/agent-auth-context", this.baseUrl);
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(url, {
|
|
headers: {
|
|
Accept: "application/json",
|
|
Authorization: credential.authorization,
|
|
"X-Project-Id": credential.projectId,
|
|
},
|
|
});
|
|
} catch {
|
|
throw new ProductServerClientError(503, "TJWater Server unavailable");
|
|
}
|
|
|
|
const text = await response.text();
|
|
const parsed = parseJson(text);
|
|
if (!response.ok) {
|
|
throw new ProductServerClientError(
|
|
response.status,
|
|
readErrorMessage(parsed, "TJWater Server rejected identity verification"),
|
|
);
|
|
}
|
|
if (!parsed || typeof parsed !== "object") {
|
|
throw new ProductServerClientError(502, "TJWater Server returned invalid JSON");
|
|
}
|
|
return parsed as AgentAuthContext;
|
|
}
|
|
}
|
|
|
|
export class ProductServerClientError extends Error {
|
|
constructor(readonly status: number, message: string) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
const parseJson = (text: string): unknown => {
|
|
if (!text.trim()) return null;
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const readErrorMessage = (value: unknown, fallback: string) => {
|
|
if (!value || typeof value !== "object") return fallback;
|
|
const record = value as Record<string, unknown>;
|
|
if (typeof record.detail === "string") return record.detail;
|
|
if (typeof record.message === "string") return record.message;
|
|
return fallback;
|
|
};
|