import { selectKeyStore } from "./key-store.js"; // PropertyData API V1 client. Every endpoint is a GET against // https://api.propertydata.co.uk/ with `key=` plus // endpoint-specific query params. Response envelope: // // { "status": "success", ...payload } // { "status": "error", "message": "..." } // // The `propertyDataCall` helper returns either the parsed payload as a // JSON string ready for tool text content, or throws PropertyDataError / // KeyNotRegisteredError so the tool wrapper can produce a deterministic // error envelope. const BASE_URL = "https://api.propertydata.co.uk"; const REQUEST_TIMEOUT_MS = 15_000; export class KeyNotRegisteredError extends Error { constructor() { super("key-not-registered"); this.name = "KeyNotRegisteredError"; } } export class PropertyDataError extends Error { constructor( public readonly status: number, public readonly endpoint: string, public readonly upstreamMessage: string, ) { super(`http-${status} from PropertyData endpoint=${endpoint}: ${upstreamMessage}`); this.name = "PropertyDataError"; } } export class BadResponseError extends Error { constructor(public readonly endpoint: string, public readonly length: number) { super(`bad-response from PropertyData endpoint=${endpoint} (non-JSON, length=${length})`); this.name = "BadResponseError"; } } export interface PropertyDataKey { createdAt: string; updatedAt: string; } export async function loadAccountKey(accountId: string): Promise { const stored = await selectKeyStore().load(accountId); if (!stored) return null; return { createdAt: stored.createdAt, updatedAt: stored.updatedAt }; } export async function resolveAccountApiKey(accountId: string): Promise { // Bypass for one-shot CLI runs: explicit env var beats stored key. const envKey = process.env.PROPERTY_DATA_API_KEY; if (envKey && envKey.trim()) return envKey.trim(); const stored = await selectKeyStore().load(accountId); if (!stored) throw new KeyNotRegisteredError(); return stored.apiKey; } export interface CallOptions { endpoint: string; toolName: string; accountId: string; params: Record; } export async function propertyDataCall(opts: CallOptions): Promise { const { endpoint, toolName, accountId, params } = opts; const apiKey = await resolveAccountApiKey(accountId); const search = new URLSearchParams(); search.set("key", apiKey); for (const [k, v] of Object.entries(params)) { if (v === undefined || v === null || v === "") continue; search.set(k, String(v)); } const url = `${BASE_URL}/${endpoint}?${search.toString()}`; const postcode = params.postcode ?? ""; const start = Date.now(); const response = await fetch(url, { headers: { Accept: "application/json" }, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), }); const ms = Date.now() - start; const statusTag = response.ok ? "ok" : `http-${Math.floor(response.status / 100)}xx`; console.error( `[property-data] tool=${toolName} account=${accountId} postcode=${postcode} status=${statusTag} ms=${ms}`, ); if (!response.ok) { const body = await response.text().catch(() => ""); throw new PropertyDataError(response.status, endpoint, body.slice(0, 200)); } const text = await response.text(); let parsed: unknown; try { parsed = JSON.parse(text); } catch { throw new BadResponseError(endpoint, text.length); } if ( parsed && typeof parsed === "object" && !Array.isArray(parsed) && "status" in parsed && (parsed as { status: unknown }).status === "error" ) { const msg = String((parsed as { message?: unknown }).message ?? "unknown error"); throw new PropertyDataError(response.status, endpoint, msg); } return JSON.stringify(parsed, null, 2); }