545 lines
24 KiB
TypeScript
545 lines
24 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
|
|
const CONTRACT_VERSION = "provider-profile-management-v1";
|
|
const DEFAULT_AGENTRUN_MGR_URL = "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080";
|
|
const AGENTRUN_MANAGER_HOST_PATTERN = /^agentrun-mgr\.[a-z0-9]([-a-z0-9]*[a-z0-9])?\.svc\.cluster\.local$/u;
|
|
const DEFAULT_TIMEOUT_MS = 30000;
|
|
|
|
const PROVIDER_PROFILE_ALIASES = Object.freeze({ codex: "codex-api" });
|
|
const PROVIDER_PROFILE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/u;
|
|
const RESERVED_PROVIDER_PROFILES = new Set(["runtime-default"]);
|
|
|
|
export async function handleProviderProfileCatalogHttp(request, response, options = {}) {
|
|
try {
|
|
const auth = await authenticateActor(request, options);
|
|
if (!auth.ok) return sendJson(response, numericStatus(auth.status) ?? numericStatus(auth.statusCode) ?? 401, auth);
|
|
|
|
const requestId = requestIdFor(request);
|
|
const env = options.env ?? process.env;
|
|
const managerUrl = resolveAgentRunManagerUrl(env);
|
|
const agentRunApiKey = resolveAgentRunApiKey(env);
|
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
const timeoutMs = positiveInteger(env.HWLAB_PROVIDER_PROFILE_AGENTRUN_HTTP_TIMEOUT_MS, positiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, DEFAULT_TIMEOUT_MS));
|
|
const delegated = await agentRunJson(fetchImpl, managerUrl, "/api/v1/provider-profiles", { method: "GET", requestId, timeoutMs, apiKey: agentRunApiKey });
|
|
const data = normalizeCatalogData(delegated.data);
|
|
const status = delegated.ok ? delegated.httpStatus || 200 : delegated.httpStatus || 502;
|
|
const base = {
|
|
ok: delegated.ok,
|
|
status: delegated.ok ? "ok" : "failed",
|
|
contractVersion: CONTRACT_VERSION,
|
|
actor: actorSummary(auth.actor),
|
|
valuesPrinted: false
|
|
};
|
|
if (delegated.ok) {
|
|
return sendJson(response, status, { ...base, items: data.items, count: data.count, data });
|
|
}
|
|
return sendJson(response, status, {
|
|
...base,
|
|
error: delegatedError(delegated),
|
|
data: null
|
|
});
|
|
} catch (error) {
|
|
return sendError(response, error.httpStatus ?? 500, error.code ?? "provider_profile_catalog_failed", error.message ?? "Provider profile catalog failed", error.details);
|
|
}
|
|
}
|
|
|
|
export async function handleProviderProfilesHttp(request, response, url, options = {}) {
|
|
try {
|
|
const auth = await authenticateAdmin(request, options);
|
|
if (!auth.ok) return sendJson(response, numericStatus(auth.status) ?? numericStatus(auth.statusCode) ?? 401, auth);
|
|
|
|
const requestId = requestIdFor(request);
|
|
const route = parseProviderProfileRoute(url.pathname, request.method);
|
|
const env = options.env ?? process.env;
|
|
const managerUrl = resolveAgentRunManagerUrl(env);
|
|
const agentRunApiKey = resolveAgentRunApiKey(env);
|
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
const timeoutMs = positiveInteger(env.HWLAB_PROVIDER_PROFILE_AGENTRUN_HTTP_TIMEOUT_MS, positiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, DEFAULT_TIMEOUT_MS));
|
|
|
|
if (route.kind === "list") {
|
|
const delegated = await agentRunJson(fetchImpl, managerUrl, "/api/v1/provider-profiles", { method: "GET", requestId, timeoutMs, apiKey: agentRunApiKey });
|
|
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeListData(delegated.data));
|
|
}
|
|
|
|
if (route.kind === "remove") {
|
|
const delegated = await agentRunJson(fetchImpl, managerUrl, `/api/v1/provider-profiles/${encodeURIComponent(route.agentRunProfile)}`, { method: "DELETE", requestId, timeoutMs, apiKey: agentRunApiKey });
|
|
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeProfileData(delegated.data));
|
|
}
|
|
|
|
if (route.kind === "credential") {
|
|
const body = await readJsonObject(request, options.bodyLimitBytes);
|
|
const credentialApiKey = text(body.apiKey);
|
|
const authJson = typeof body.authJson === "string" ? body.authJson : "";
|
|
if (credentialApiKey.length < 8 && !authJson.trim()) return sendError(response, 400, "credential_required", "apiKey or authJson is required and must not be empty");
|
|
const delegatedBody = {
|
|
...(authJson.trim() ? { authJson } : { apiKey: credentialApiKey }),
|
|
...(body.config && typeof body.config === "object" ? { config: body.config } : {}),
|
|
delegatedBy: delegatedBy(auth, requestId),
|
|
reason: text(body.reason) || "hwlab-provider-management"
|
|
};
|
|
const delegated = await agentRunJson(fetchImpl, managerUrl, `/api/v1/provider-profiles/${encodeURIComponent(route.agentRunProfile)}/credential`, { method: "PUT", body: delegatedBody, requestId, timeoutMs, apiKey: agentRunApiKey });
|
|
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeProfileData(delegated.data));
|
|
}
|
|
|
|
if (route.kind === "config") {
|
|
if (route.method === "GET") {
|
|
const delegated = await agentRunJson(fetchImpl, managerUrl, `/api/v1/provider-profiles/${encodeURIComponent(route.agentRunProfile)}/config`, { method: "GET", requestId, timeoutMs, apiKey: agentRunApiKey });
|
|
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeConfigData(delegated.data), { allowConfigToml: true });
|
|
}
|
|
const body = await readJsonObject(request, options.bodyLimitBytes);
|
|
const configToml = typeof body.configToml === "string" ? body.configToml : "";
|
|
if (!configToml.trim()) return sendError(response, 400, "config_toml_required", "configToml is required and must not be empty");
|
|
const delegatedBody = {
|
|
configToml,
|
|
delegatedBy: delegatedBy(auth, requestId),
|
|
reason: text(body.reason) || "hwlab-provider-management"
|
|
};
|
|
const delegated = await agentRunJson(fetchImpl, managerUrl, `/api/v1/provider-profiles/${encodeURIComponent(route.agentRunProfile)}/config`, { method: "PUT", body: delegatedBody, requestId, timeoutMs, apiKey: agentRunApiKey });
|
|
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeConfigData(delegated.data));
|
|
}
|
|
|
|
if (route.kind === "validate") {
|
|
const body = await readOptionalJsonObject(request, options.bodyLimitBytes);
|
|
const delegatedBody = {
|
|
...(text(body.prompt) ? { prompt: text(body.prompt) } : {}),
|
|
delegatedBy: delegatedBy(auth, requestId),
|
|
reason: text(body.reason) || "hwlab-provider-management"
|
|
};
|
|
const delegated = await agentRunJson(fetchImpl, managerUrl, `/api/v1/provider-profiles/${encodeURIComponent(route.agentRunProfile)}/validate`, { method: "POST", body: delegatedBody, requestId, timeoutMs, apiKey: agentRunApiKey });
|
|
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeValidationData(delegated.data, route.publicProfile));
|
|
}
|
|
|
|
if (route.kind === "validation") {
|
|
const delegated = await agentRunJson(fetchImpl, managerUrl, `/api/v1/provider-profiles/${encodeURIComponent(route.agentRunProfile)}/validations/${encodeURIComponent(route.validationId)}`, { method: "GET", requestId, timeoutMs, apiKey: agentRunApiKey });
|
|
return sendDelegated(response, delegated, auth, route, managerUrl, requestId, normalizeValidationData(delegated.data, route.publicProfile));
|
|
}
|
|
|
|
return sendError(response, 404, "not_found", "Provider profile route is not implemented");
|
|
} catch (error) {
|
|
return sendError(response, error.httpStatus ?? 500, error.code ?? "provider_profile_management_failed", error.message ?? "Provider profile management failed", error.details);
|
|
}
|
|
}
|
|
|
|
async function authenticateAdmin(request, options) {
|
|
const auth = await authenticateActor(request, options);
|
|
if (!auth.ok) return auth;
|
|
if (auth.actor?.role !== "admin") return errorPayload("admin_required", "Only admin users can manage provider profiles", 403);
|
|
return auth;
|
|
}
|
|
|
|
async function authenticateActor(request, options) {
|
|
const controller = options.accessController;
|
|
if (!controller || typeof controller.authenticate !== "function") {
|
|
return errorPayload("access_controller_unavailable", "HWLAB access controller is not available", 503);
|
|
}
|
|
const auth = await controller.authenticate(request, { required: true });
|
|
if (!auth.ok) return auth;
|
|
return auth;
|
|
}
|
|
|
|
function parseProviderProfileRoute(pathname, method) {
|
|
if (pathname === "/v1/admin/provider-profiles") {
|
|
if (method !== "GET") throw routeError(405, "method_not_allowed", "Provider profiles collection only supports GET");
|
|
return { kind: "list", publicProfile: null, agentRunProfile: null };
|
|
}
|
|
const match = pathname.match(/^\/v1\/admin\/provider-profiles\/([^/]+)(?:\/(credential|config|validate|validations\/([^/]+)))?$/u);
|
|
if (!match) throw routeError(404, "not_found", "Provider profile route is not implemented");
|
|
const profile = normalizeProfile(decodeURIComponent(match[1]));
|
|
const suffix = match[2] ?? "";
|
|
if (suffix === "credential") {
|
|
if (method !== "PUT") throw routeError(405, "method_not_allowed", "Provider profile credential only supports PUT");
|
|
return { kind: "credential", ...profile };
|
|
}
|
|
if (!suffix) {
|
|
if (method !== "DELETE") throw routeError(405, "method_not_allowed", "Provider profile root only supports DELETE");
|
|
return { kind: "remove", ...profile };
|
|
}
|
|
if (suffix === "config") {
|
|
if (method !== "GET" && method !== "PUT") throw routeError(405, "method_not_allowed", "Provider profile config only supports GET and PUT");
|
|
return { kind: "config", method, ...profile };
|
|
}
|
|
if (suffix === "validate") {
|
|
if (method !== "POST") throw routeError(405, "method_not_allowed", "Provider profile validation only supports POST");
|
|
return { kind: "validate", ...profile };
|
|
}
|
|
if (suffix.startsWith("validations/")) {
|
|
if (method !== "GET") throw routeError(405, "method_not_allowed", "Provider profile validation lookup only supports GET");
|
|
return { kind: "validation", ...profile, validationId: decodeURIComponent(match[3] ?? "") };
|
|
}
|
|
throw routeError(404, "not_found", "Provider profile route is not implemented");
|
|
}
|
|
|
|
function normalizeProfile(value) {
|
|
const input = text(value).toLowerCase();
|
|
const publicProfile = PROVIDER_PROFILE_ALIASES[input] ?? input;
|
|
if (!publicProfile || RESERVED_PROVIDER_PROFILES.has(publicProfile) || !PROVIDER_PROFILE_ID_PATTERN.test(publicProfile)) {
|
|
throw routeError(400, "invalid_provider_profile", "Provider profile must be a lowercase slug such as deepseek, minimax-m3, dsflash-go, or codex-api", {
|
|
profile: input,
|
|
alias: "codex -> codex-api",
|
|
pattern: String(PROVIDER_PROFILE_ID_PATTERN)
|
|
});
|
|
}
|
|
return { publicProfile, agentRunProfile: publicProfile === "codex-api" ? "codex" : publicProfile };
|
|
}
|
|
|
|
async function agentRunJson(fetchImpl, managerUrl, path, { method = "GET", body, requestId, timeoutMs, apiKey }) {
|
|
const startedAt = Date.now();
|
|
const targetUrl = `${managerUrl}${path}`;
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
try {
|
|
const headers: Record<string, string> = {
|
|
accept: "application/json",
|
|
"content-type": "application/json",
|
|
"x-request-id": requestId,
|
|
"x-source-service-id": "hwlab-cloud-api",
|
|
"x-hwlab-delegation": "provider-profile-management-v1"
|
|
};
|
|
if (apiKey) headers.authorization = `Bearer ${apiKey}`;
|
|
const response = await fetchImpl(targetUrl, {
|
|
method,
|
|
headers,
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
signal: controller.signal
|
|
});
|
|
const textBody = await response.text();
|
|
const payload = parseJson(textBody);
|
|
const ok = response.ok && payload?.ok !== false;
|
|
return {
|
|
ok,
|
|
httpStatus: response.status,
|
|
data: payload?.data ?? payload,
|
|
payload,
|
|
traceId: text(payload?.traceId),
|
|
elapsedMs: Date.now() - startedAt,
|
|
method,
|
|
path
|
|
};
|
|
} catch (error) {
|
|
const timedOut = controller.signal.aborted || error?.name === "AbortError";
|
|
return {
|
|
ok: false,
|
|
httpStatus: 0,
|
|
data: null,
|
|
payload: { ok: false, error: { code: timedOut ? "agentrun_timeout" : "agentrun_request_failed", message: error?.message ?? String(error) } },
|
|
traceId: "",
|
|
elapsedMs: Date.now() - startedAt,
|
|
method,
|
|
path,
|
|
transportError: { code: timedOut ? "agentrun_timeout" : "agentrun_request_failed", message: error?.message ?? String(error), valuesPrinted: false }
|
|
};
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
function sendDelegated(response, delegated, auth, route, managerUrl, requestId, data, options = {}) {
|
|
const mutation = route.kind === "remove" || route.kind === "credential" || (route.kind === "config" && route.method === "PUT");
|
|
const status = delegated.ok ? (mutation ? 200 : delegated.httpStatus || 200) : delegated.httpStatus || 502;
|
|
const base = {
|
|
ok: delegated.ok,
|
|
status: delegated.ok ? (route.kind === "remove" ? "removed" : mutation ? "updated" : "ok") : "failed",
|
|
contractVersion: CONTRACT_VERSION,
|
|
actor: actorSummary(auth.actor),
|
|
profile: route.publicProfile ?? undefined,
|
|
delegation: delegationSummary(managerUrl, delegated, requestId),
|
|
valuesPrinted: false
|
|
};
|
|
if (delegated.ok) return sendJson(response, status, { ...base, ...listAliases(data), data }, options);
|
|
return sendJson(response, status, {
|
|
...base,
|
|
error: delegatedError(delegated),
|
|
data: null
|
|
});
|
|
}
|
|
|
|
function normalizeListData(data) {
|
|
const sourceItems = Array.isArray(data?.items) ? data.items : [];
|
|
const items = sourceItems.map(normalizeProfileData);
|
|
return { ...redactObject(data), items, count: items.length, valuesPrinted: false };
|
|
}
|
|
|
|
function normalizeCatalogData(data) {
|
|
const sourceItems = Array.isArray(data?.items) ? data.items : [];
|
|
const items = sourceItems.map(normalizeCatalogProfileData);
|
|
return { items, count: items.length, valuesPrinted: false };
|
|
}
|
|
|
|
function normalizeProfileData(item) {
|
|
if (!item || typeof item !== "object") return item;
|
|
const copy = redactObject(item);
|
|
if (copy.profile === "codex") {
|
|
copy.backendProfile = "codex";
|
|
copy.profile = "codex-api";
|
|
}
|
|
if (copy.profile === "deepseek") copy.bridge = deepseekBridgeSummary();
|
|
if (text(copy.configSummary?.model ?? copy.model) === "deepseek-v4-flash") copy.bridge = opencodeZenGoBridgeSummary();
|
|
copy.valuesPrinted = false;
|
|
return copy;
|
|
}
|
|
|
|
function normalizeCatalogProfileData(item) {
|
|
const normalized = normalizeProfileData(item);
|
|
if (!normalized || typeof normalized !== "object") return normalized;
|
|
return {
|
|
profile: normalized.profile,
|
|
backendProfile: normalized.backendProfile,
|
|
backendKind: normalized.backendKind,
|
|
configured: normalized.configured,
|
|
failureKind: normalized.failureKind ?? null,
|
|
bridge: normalized.bridge,
|
|
valuesPrinted: false
|
|
};
|
|
}
|
|
|
|
function normalizeConfigData(data) {
|
|
if (!data || typeof data !== "object") return data;
|
|
const configToml = typeof data.configToml === "string" ? data.configToml : typeof data["config.toml"] === "string" ? data["config.toml"] : undefined;
|
|
const copy = normalizeProfileData(data);
|
|
if (typeof configToml === "string") copy.configToml = configToml;
|
|
copy.configTomlPrinted = typeof configToml === "string";
|
|
copy.credentialValuesPrinted = false;
|
|
copy.valuesPrinted = false;
|
|
return copy;
|
|
}
|
|
|
|
function normalizeValidationData(data, fallbackProfile) {
|
|
if (!data || typeof data !== "object") return data;
|
|
const copy = normalizeProfileData(data);
|
|
if (!copy.profile) copy.profile = fallbackProfile;
|
|
if (copy.result && typeof copy.result === "object") copy.result = redactObject(copy.result);
|
|
if (copy.runnerJobs && Array.isArray(copy.runnerJobs)) copy.runnerJobs = copy.runnerJobs.map(redactObject);
|
|
return copy;
|
|
}
|
|
|
|
function listAliases(data) {
|
|
return Array.isArray(data?.items) ? { items: data.items, count: data.count ?? data.items.length } : {};
|
|
}
|
|
|
|
function delegatedError(delegated) {
|
|
const body = delegated.payload && typeof delegated.payload === "object" ? delegated.payload : {};
|
|
const nested = body.error && typeof body.error === "object" ? body.error : {};
|
|
return {
|
|
code: text(nested.code ?? body.code) || "agentrun_delegation_failed",
|
|
message: text(nested.message ?? body.message) || `AgentRun provider profile delegation failed with HTTP ${delegated.httpStatus}`,
|
|
failureKind: text(nested.failureKind ?? body.failureKind) || undefined,
|
|
agentRun: {
|
|
httpStatus: delegated.httpStatus,
|
|
traceId: delegated.traceId || undefined,
|
|
route: { method: delegated.method, path: delegated.path },
|
|
elapsedMs: delegated.elapsedMs,
|
|
transportError: delegated.transportError ? redactObject(delegated.transportError) : undefined,
|
|
valuesPrinted: false
|
|
},
|
|
valuesPrinted: false
|
|
};
|
|
}
|
|
|
|
function delegatedBy(auth, requestId) {
|
|
return {
|
|
system: "hwlab-v02",
|
|
userId: text(auth.actor?.id),
|
|
username: text(auth.actor?.username),
|
|
authMethod: text(auth.authMethod) || (auth.apiKey ? "api-key" : "web-session"),
|
|
requestId,
|
|
valuesPrinted: false
|
|
};
|
|
}
|
|
|
|
function actorSummary(actor) {
|
|
return actor ? {
|
|
id: text(actor.id),
|
|
username: text(actor.username),
|
|
displayName: text(actor.displayName),
|
|
role: text(actor.role),
|
|
status: text(actor.status),
|
|
valuesPrinted: false
|
|
} : null;
|
|
}
|
|
|
|
function delegationSummary(managerUrl, delegated, requestId) {
|
|
const url = new URL(managerUrl);
|
|
return {
|
|
provider: "agentrun-v01",
|
|
requestId,
|
|
agentRunBaseUrl: redactUrl(managerUrl),
|
|
agentRunHost: url.hostname,
|
|
traceId: delegated.traceId || undefined,
|
|
httpStatus: delegated.httpStatus,
|
|
elapsedMs: delegated.elapsedMs,
|
|
valuesPrinted: false
|
|
};
|
|
}
|
|
|
|
function deepseekBridgeSummary() {
|
|
return {
|
|
kind: "moon-bridge",
|
|
route: "HWLAB Moon Bridge -> DeepSeek official upstream",
|
|
serviceUrl: "http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1",
|
|
responsesPath: "/v1/responses",
|
|
forbiddenHosts: ["hyueapi.com"],
|
|
valuesPrinted: false
|
|
};
|
|
}
|
|
|
|
function opencodeZenGoBridgeSummary() {
|
|
return {
|
|
kind: "moon-bridge",
|
|
route: "HWLAB Moon Bridge -> OpenCode Zen Go Chat Completions",
|
|
serviceUrl: "http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1",
|
|
responsesPath: "/v1/responses",
|
|
model: "deepseek-v4-flash",
|
|
upstreamProvider: "opencode",
|
|
upstreamProtocol: "openai-chat",
|
|
forbiddenHosts: ["hyueapi.com"],
|
|
valuesPrinted: false
|
|
};
|
|
}
|
|
|
|
function resolveAgentRunManagerUrl(env) {
|
|
const raw = text(env.AGENTRUN_MGR_URL) || text(env.HWLAB_CODE_AGENT_AGENTRUN_MGR_URL) || text(env.AGENTRUN_MANAGER_URL) || DEFAULT_AGENTRUN_MGR_URL;
|
|
let parsed;
|
|
try {
|
|
parsed = new URL(raw);
|
|
} catch {
|
|
throw routeError(503, "agentrun_manager_url_invalid", "AgentRun manager URL is invalid", { managerUrl: redactUrl(raw) });
|
|
}
|
|
const allowLocal = truthy(env.HWLAB_PROVIDER_PROFILE_AGENTRUN_ALLOW_NON_K3S_URL) || truthy(env.HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL);
|
|
const localAllowed = allowLocal && ["127.0.0.1", "localhost"].includes(parsed.hostname);
|
|
if (!isAgentRunManagerInternalServiceHost(parsed.hostname) && !localAllowed) {
|
|
throw routeError(503, "agentrun_manager_url_invalid", "AgentRun manager URL must use internal k3s Service DNS agentrun-mgr.<namespace>.svc.cluster.local", { managerUrl: redactUrl(raw) });
|
|
}
|
|
return raw.replace(/\/+$/u, "");
|
|
}
|
|
|
|
function resolveAgentRunApiKey(env) {
|
|
return text(env.AGENTRUN_API_KEY) || text(env.HWLAB_CODE_AGENT_AGENTRUN_API_KEY) || text(env.HWLAB_PROVIDER_PROFILE_AGENTRUN_API_KEY) || text(env.HWLAB_API_KEY);
|
|
}
|
|
|
|
function isAgentRunManagerInternalServiceHost(host) {
|
|
return AGENTRUN_MANAGER_HOST_PATTERN.test(String(host ?? "").trim().toLowerCase());
|
|
}
|
|
|
|
async function readOptionalJsonObject(request, limitBytes) {
|
|
const body = await readBody(request, limitBytes);
|
|
if (!body.trim()) return {};
|
|
return parseJsonObject(body);
|
|
}
|
|
|
|
async function readJsonObject(request, limitBytes) {
|
|
return parseJsonObject(await readBody(request, limitBytes));
|
|
}
|
|
|
|
async function readBody(request, limitBytes = 1024 * 1024) {
|
|
const chunks = [];
|
|
let total = 0;
|
|
for await (const chunk of request) {
|
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
total += buffer.length;
|
|
if (total > limitBytes) throw routeError(413, "body_too_large", "Request body is too large");
|
|
chunks.push(buffer);
|
|
}
|
|
return Buffer.concat(chunks).toString("utf8");
|
|
}
|
|
|
|
function parseJsonObject(body) {
|
|
const parsed = parseJson(body);
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw routeError(400, "invalid_json_body", "Request body must be a JSON object");
|
|
return parsed;
|
|
}
|
|
|
|
function parseJson(value) {
|
|
try {
|
|
return value ? JSON.parse(value) : null;
|
|
} catch (error) {
|
|
throw routeError(400, "parse_error", "Invalid JSON body", { reason: error?.message ?? String(error) });
|
|
}
|
|
}
|
|
|
|
function redactObject(value, key = "") {
|
|
if (Array.isArray(value)) return value.map((item) => redactObject(item, key));
|
|
if (value && typeof value === "object") {
|
|
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, redactObject(entryValue, entryKey)]));
|
|
}
|
|
if (typeof value === "string" && (secretField(key) || looksSecret(value))) return "[redacted]";
|
|
return value;
|
|
}
|
|
|
|
function secretField(key) {
|
|
const lower = key.toLowerCase();
|
|
if (["apikey", "api_key", "authorization", "authjson", "auth.json", "configtoml", "config.toml", "stringdata"].includes(lower)) return true;
|
|
if (lower === "secretref" || lower === "secretname" || lower === "secretkey" || lower === "keys") return false;
|
|
return lower.includes("password") || lower.endsWith("token");
|
|
}
|
|
|
|
function looksSecret(value) {
|
|
return /^sk-[A-Za-z0-9_-]{10,}/u.test(value) || /^Bearer\s+\S+/u.test(value);
|
|
}
|
|
|
|
function requestIdFor(request) {
|
|
return getHeader(request, "x-request-id") || `req_provider_${randomUUID().replace(/-/gu, "")}`;
|
|
}
|
|
|
|
function getHeader(request, name) {
|
|
const value = request.headers?.[name.toLowerCase()];
|
|
if (Array.isArray(value)) return text(value[0]);
|
|
return text(value);
|
|
}
|
|
|
|
function positiveInteger(value, fallback) {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
}
|
|
|
|
function numericStatus(value) {
|
|
const parsed = Number(value);
|
|
return Number.isInteger(parsed) && parsed >= 100 && parsed <= 599 ? parsed : null;
|
|
}
|
|
|
|
function truthy(value) {
|
|
return ["1", "true", "yes", "on"].includes(text(value).toLowerCase());
|
|
}
|
|
|
|
function redactUrl(value) {
|
|
try {
|
|
const url = new URL(value);
|
|
if (url.username) url.username = "redacted";
|
|
if (url.password) url.password = "redacted";
|
|
return url.toString().replace(/\/$/u, "");
|
|
} catch {
|
|
return "[invalid-url]";
|
|
}
|
|
}
|
|
|
|
function routeError(httpStatus, code, message, details = {}) {
|
|
return Object.assign(new Error(message), { httpStatus, code, details });
|
|
}
|
|
|
|
function errorPayload(code, message, status, details = {}) {
|
|
return { ok: false, status: "failed", contractVersion: CONTRACT_VERSION, error: { code, message, ...details, valuesPrinted: false }, valuesPrinted: false, statusCode: status };
|
|
}
|
|
|
|
function sendError(response, statusCode, code, message, details = {}) {
|
|
return sendJson(response, statusCode, { ok: false, status: "failed", contractVersion: CONTRACT_VERSION, error: { code, message, ...redactObject(details), valuesPrinted: false }, valuesPrinted: false });
|
|
}
|
|
|
|
function sendJson(response, statusCode, payload, options = {}) {
|
|
response.writeHead(statusCode, { "content-type": "application/json" });
|
|
const body = options.allowConfigToml ? redactObjectAllowingConfigToml(payload) : redactObject(payload);
|
|
response.end(`${JSON.stringify(body)}\n`);
|
|
}
|
|
|
|
function redactObjectAllowingConfigToml(value, key = "") {
|
|
if ((key === "configToml" || key === "config.toml") && typeof value === "string") return value;
|
|
if (Array.isArray(value)) return value.map((item) => redactObjectAllowingConfigToml(item, key));
|
|
if (value && typeof value === "object") {
|
|
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, redactObjectAllowingConfigToml(entryValue, entryKey)]));
|
|
}
|
|
if (typeof value === "string" && (secretField(key) || looksSecret(value))) return "[redacted]";
|
|
return value;
|
|
}
|
|
|
|
function text(value) {
|
|
return String(value ?? "").trim();
|
|
}
|