78 lines
2.8 KiB
JavaScript
78 lines
2.8 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const ALLOWED_BROKER_HOSTS = new Set(["127.0.0.1", "localhost"]);
|
|
const DEFAULT_TIMEOUT_MS = 3000;
|
|
|
|
export function brokerCredentialFromPath(credentialPath) {
|
|
if (!path.isAbsolute(credentialPath)) throw new Error("broker_credential_path_invalid");
|
|
const stat = fs.lstatSync(credentialPath);
|
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("broker_credential_path_invalid");
|
|
if (process.platform !== "win32" && (stat.mode & 0o077) !== 0) {
|
|
throw new Error("broker_credential_permissions_invalid");
|
|
}
|
|
const token = fs.readFileSync(credentialPath, "utf8").trim();
|
|
if (!token) throw new Error("broker_credential_empty");
|
|
return token;
|
|
}
|
|
|
|
export function assertLocalBrokerEndpoint(endpoint) {
|
|
const safe = new URL(endpoint);
|
|
if (safe.protocol !== "http:") throw new Error("broker_endpoint_protocol_invalid");
|
|
if (!ALLOWED_BROKER_HOSTS.has(safe.hostname)) throw new Error("broker_endpoint_host_invalid");
|
|
if (safe.username || safe.password || safe.search || safe.hash) throw new Error("broker_endpoint_invalid");
|
|
if (safe.pathname === "/") safe.pathname = "/v1/sessions/provision";
|
|
if (safe.pathname !== "/v1/sessions/provision") throw new Error("broker_endpoint_path_invalid");
|
|
return safe;
|
|
}
|
|
|
|
export async function callBrokerProvision({
|
|
endpoint,
|
|
credentialPath,
|
|
payload,
|
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
}) {
|
|
const brokerUrl = assertLocalBrokerEndpoint(endpoint);
|
|
const credential = brokerCredentialFromPath(credentialPath);
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), Math.min(Math.max(Number(timeoutMs) || DEFAULT_TIMEOUT_MS, 500), 15_000));
|
|
try {
|
|
const response = await fetch(brokerUrl, {
|
|
method: "POST",
|
|
signal: controller.signal,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${credential}`,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const responseBody = await response.text();
|
|
let body;
|
|
try {
|
|
body = responseBody ? JSON.parse(responseBody) : {};
|
|
} catch {
|
|
throw new Error("broker_response_not_json");
|
|
}
|
|
if (!response.ok) {
|
|
const reason = typeof body?.error === "string" ? body.error : `http_${response.status}`;
|
|
const error = new Error(reason);
|
|
error.code = "broker_http_error";
|
|
error.status = response.status;
|
|
throw error;
|
|
}
|
|
return body;
|
|
} catch (error) {
|
|
if (error.name === "AbortError") {
|
|
const timeout = new Error("broker_timeout");
|
|
timeout.code = "broker_timeout";
|
|
throw timeout;
|
|
}
|
|
if (error.code) throw error;
|
|
const transport = new Error("broker_unreachable");
|
|
transport.code = "broker_unreachable";
|
|
throw transport;
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|