diff --git a/plugins/aios-tenant-boundary/session-broker-client.js b/plugins/aios-tenant-boundary/session-broker-client.js new file mode 100644 index 0000000..d3967aa --- /dev/null +++ b/plugins/aios-tenant-boundary/session-broker-client.js @@ -0,0 +1,76 @@ +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"; + 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); + } +}