cli/curl.ts
Rama main · réplica de lectura del repositorio original.
import type {
AdminApiRequest,
AdminApiResponse,
AdminApiTransport,
} from "#shared/admin-api-client.ts";
import type { CliConfig } from "./config.ts";
import { CliError } from "./errors.ts";
const buildUrl = (host: string, path: string): string =>
`${host}${path.startsWith("/") ? path : `/${path}`}`;
export const buildCurlArgs = (
config: CliConfig,
{ body, method, path }: AdminApiRequest,
): string[] => {
const args = [
"--silent",
"--show-error",
"--request",
method,
"--header",
`Authorization: Bearer ${config.apiKey}`,
"--header",
"Accept: application/json",
// The status rides after the body, so the transport can answer with it
// and the client can refuse a non-2xx status by name.
"--write-out",
"\n%{http_code}",
];
if (body !== undefined) {
args.push(
"--header",
"Content-Type: application/json",
"--data",
JSON.stringify(body),
);
}
args.push(buildUrl(config.apiHostname, path));
return args;
};
export const curlFailureMessage = (stderr: string, stdout: string): string => {
const stderrText = stderr.trim();
if (stderrText) return stderrText;
const stdoutText = stdout.trim();
return stdoutText || "curl request failed";
};
/** Split curl's answer into the response body and the status line the
* --write-out argument appended. */
const splitStatus = (stdout: string): { body: string; statusText: string } => {
// The --write-out string always rides last, after one newline; a failed
// transfer carries the code 000.
const split = stdout.lastIndexOf("\n");
return {
body: stdout.slice(0, split),
statusText: stdout.slice(split + 1).trim(),
};
};
/** Parse the answer body. A non-JSON body on an error status is the
* server's refusal: the raw body goes back, and the client's status
* mapping names the kind (401 → not_allowed, 400 → validation). A non-JSON
* success keeps the status the transport already extracted: the server
* answered, so the contract's status is its HTTP status, not null. */
const parseJson = (text: string, status: number): unknown => {
try {
return JSON.parse(text) as unknown;
} catch {
if (status >= 400) return text;
throw new CliError(
"server",
`The server answered with text that is not JSON: ${text.slice(0, 200)}`,
null,
status,
);
}
};
/** The curl transport: one request in, the parsed body and the HTTP status
* out. A transport failure throws; the client refuses a non-2xx status
* itself, including the 000 a transfer without an answer reports. */
export const curlJson = async (
config: CliConfig,
options: AdminApiRequest,
): Promise<AdminApiResponse> => {
const command = new Deno.Command("curl", {
args: buildCurlArgs(config, options),
stderr: "piped",
stdout: "piped",
});
const output = await command.output();
const stdout = new TextDecoder().decode(output.stdout);
const stderr = new TextDecoder().decode(output.stderr);
const { body, statusText } = splitStatus(stdout);
if (!output.success) {
// The server kind survives the importer's io fallback: a transfer
// failure is never a local input failure.
throw new CliError("server", curlFailureMessage(stderr, body));
}
const status = Number(statusText);
return { data: parseJson(body, status), status };
};
/** The transport one resolved config serves: curl carries the auth and the
* HTTP mechanics, and the client validates every answer. */
export const curlTransport =
(config: CliConfig): AdminApiTransport =>
(options) =>
curlJson(config, options);