feat(context): verify product session scope

This commit is contained in:
2026-09-22 10:43:36 +08:00
parent 88b41ba04c
commit 9faed59f23
12 changed files with 358 additions and 56 deletions
+65
View File
@@ -0,0 +1,65 @@
import { config } from "../config.js";
import type { components } from "../generated/server-api.js";
export type ProductCredential = {
authorization: string;
projectId: string;
};
export type AgentAuthContext = components["schemas"]["AgentAuthContextResponse"];
export class ProductServerClient {
constructor(private readonly baseUrl = config.TJWATER_SERVER_BASE_URL) {}
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;
};