import { t } from "../i18n.js";
const RPC_TIMEOUT_MS = 60_000;
const UPLOAD_TIMEOUT_MS = 10 * 60_000;
async function fetchTimeout(url, init, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
try {
const httpResponse = await fetch(url, { ...init, signal: abortController.signal });
const text = await httpResponse.text();
return { ok: httpResponse.ok, status: httpResponse.status, json: async () => JSON.parse(text) };
} catch (error) {
if (abortController.signal.aborted) throw new Error(t("tempo esgotado (") + Math.round(timeoutMs / 1000) + t("s) sem resposta do daemon"));
throw error;
} finally {
clearTimeout(timer);
}
}
const ACCESS_KEY_SEPARATOR = "::";
export function decodeAccessKey(accessKey) {
const trimmed = (accessKey || "").trim();
if (!trimmed) return null;
let text;
try {
const base64 = trimmed.replace(/=+$/, "").replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(base64 + "===".slice((base64.length + 3) % 4));
text = new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
} catch {
return null;
}
const authorityStart = text.includes("://") ? text.indexOf("://") + 3 : 0;
const searchFrom = text[authorityStart] === "[" ? text.indexOf("]", authorityStart) : authorityStart;
const at = searchFrom < 0 ? -1 : text.indexOf(ACCESS_KEY_SEPARATOR, searchFrom);
if (at < 0) return null;
const address = text.slice(authorityStart, at).trim().replace(/\/+$/, "");
const token = text.slice(at + ACCESS_KEY_SEPARATOR.length).trim();
return address ? { address, token } : null;
}
export function encodeAccessKey(url, token) {
let address = url.replace(/\/+$/, "");
try {
const parsed = new URL(address.includes("://") ? address : "http://" + address);
address = parsed.hostname + ":" + (parsed.port || (parsed.protocol === "https:" ? "443" : "80"));
} catch {
}
return btoa(address + ACCESS_KEY_SEPARATOR + token);
}
export async function login(baseUrl, accessKey) {
let httpResponse;
try {
httpResponse = await fetchTimeout(
baseUrl.replace(/\/+$/, "") + "/api/login",
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: accessKey.trim() }) },
RPC_TIMEOUT_MS,
);
} catch (error) {
return { ok: false, error: error && error.message ? error.message : t("falha de rede") };
}
let decoded;
try {
decoded = await httpResponse.json();
} catch {
return { ok: false, error: "HTTP " + httpResponse.status };
}
if (!httpResponse.ok) {
return { ok: false, error: decoded?.error?.message || "HTTP " + httpResponse.status };
}
return { ok: true, token: decoded.access_token };
}
export class Api {
constructor(baseUrl, token) {
this.baseUrl = baseUrl.replace(/\/+$/, "");
this.token = token || "";
}
headers() {
const headers = { "Content-Type": "application/json" };
if (this.token) headers["Authorization"] = "Bearer " + this.token;
return headers;
}
async rpc(command) {
let httpResponse;
try {
httpResponse = await fetchTimeout(
this.baseUrl + "/api/rpc",
{ method: "POST", headers: this.headers(), body: JSON.stringify(command) },
RPC_TIMEOUT_MS,
);
} catch (error) {
return { ok: false, error: error && error.message ? error.message : t("falha de rede") };
}
if (!httpResponse.ok) {
return { ok: false, error: "HTTP " + httpResponse.status };
}
let decoded;
try {
decoded = await httpResponse.json();
} catch {
return { ok: false, error: t("resposta inválida do daemon") };
}
return { ok: true, value: decoded };
}
async rpcChecked(command) {
const rpcResponse = await this.rpc(command);
if (!rpcResponse.ok) return rpcResponse;
const decoded = rpcResponse.value;
if (decoded && typeof decoded === "object" && decoded.Err) {
return { ok: false, error: decoded.Err.message || decoded.Err.code || "erro" };
}
return rpcResponse;
}
async uploadArchive(serviceId, file) {
let httpResponse;
try {
const headers = { "Content-Type": "application/zip" };
if (this.token) headers["Authorization"] = "Bearer " + this.token;
headers["X-Rustploy-Filename"] = file.name || "archive.zip";
httpResponse = await fetchTimeout(
`${this.baseUrl}/api/services/${serviceId}/archive`,
{ method: "POST", headers: headers, body: file },
UPLOAD_TIMEOUT_MS,
);
} catch (error) {
return { ok: false, error: error && error.message ? error.message : t("falha de rede") };
}
if (!httpResponse.ok) {
return { ok: false, error: "HTTP " + httpResponse.status };
}
let decoded;
try {
decoded = await httpResponse.json();
} catch {
return { ok: false, error: t("resposta inválida do daemon") };
}
if (decoded && typeof decoded === "object" && decoded.Err) {
return { ok: false, error: decoded.Err.message || decoded.Err.code || "erro" };
}
return { ok: true, value: decoded };
}
}