Files
routstrd/src/utils/daemon-client.ts
T

208 lines
5.5 KiB
TypeScript

import { existsSync } from "fs";
import { startDaemon } from "../start-daemon";
import {
CONFIG_FILE,
DEFAULT_CONFIG,
type RoutstrdConfig,
} from "./config";
import {
createNIP98Authorization,
parseSecretKey,
npubFromSecretKey,
type HttpMethod,
} from "./nip98";
export interface CommandResponse {
output?: unknown;
error?: string;
}
export async function loadConfig(): Promise<RoutstrdConfig> {
try {
if (existsSync(CONFIG_FILE)) {
const content = await Bun.file(CONFIG_FILE).text();
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
}
} catch (error) {
console.error("Failed to load config:", error);
}
return DEFAULT_CONFIG;
}
export function getDaemonBaseUrl(config: RoutstrdConfig): string {
if (config.daemonUrl) {
return config.daemonUrl.replace(/\/$/, "");
}
const host = config.host === "0.0.0.0" ? "127.0.0.1" : config.host;
return `http://${host}:${config.port}`;
}
export function getAuthBaseUrl(config: RoutstrdConfig): string {
if (config.authUrl) {
return config.authUrl.replace(/\/$/, "");
}
return getDaemonBaseUrl(config);
}
async function _callUrl(
baseUrl: string,
path: string,
options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: object },
config: RoutstrdConfig,
): Promise<CommandResponse> {
const { method = "GET", body } = options;
const url = `${baseUrl}${path}`;
const bodyString = body ? JSON.stringify(body) : undefined;
const bodyBytes = bodyString
? new TextEncoder().encode(bodyString)
: undefined;
let authorization: string | undefined;
if ((config.daemonUrl || config.authUrl) && config.nsec) {
const secretKey = parseSecretKey(config.nsec);
authorization = await createNIP98Authorization(
secretKey,
url,
method as HttpMethod,
bodyBytes,
);
}
const response = await fetch(url, {
method,
headers: {
...(authorization ? { Authorization: authorization } : {}),
...(bodyString ? { "Content-Type": "application/json" } : {}),
},
body: bodyString,
});
if (!response.ok) {
const errorData = (await response.json()) as { error?: string };
throw new Error(errorData.error || `HTTP ${response.status}`);
}
return response.json() as Promise<CommandResponse>;
}
export async function callDaemon(
path: string,
options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: object } = {},
): Promise<CommandResponse> {
const config = await loadConfig();
const baseUrl = getDaemonBaseUrl(config);
return _callUrl(baseUrl, path, options, config);
}
/** Like callDaemon but sends requests to the auth proxy URL instead.
* Falls back to the daemon URL if no authUrl is configured. */
export async function callAuth(
path: string,
options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: object } = {},
): Promise<CommandResponse> {
const config = await loadConfig();
const baseUrl = getAuthBaseUrl(config);
return _callUrl(baseUrl, path, options, config);
}
export async function isDaemonRunning(): Promise<boolean> {
try {
const config = await loadConfig();
const baseUrl = getDaemonBaseUrl(config);
const url = `${baseUrl}/health`;
let authorization: string | undefined;
if (config.daemonUrl && config.nsec) {
const secretKey = parseSecretKey(config.nsec);
authorization = await createNIP98Authorization(secretKey, url, "GET");
}
const response = await fetch(url, {
headers: authorization ? { Authorization: authorization } : {},
});
return response.ok;
} catch {
return false;
}
}
export function getUserNpub(config: RoutstrdConfig): string | null {
if (!config.nsec) return null;
try {
const secretKey = parseSecretKey(config.nsec);
return npubFromSecretKey(secretKey);
} catch {
return null;
}
}
export function getNpubSuffix(config: RoutstrdConfig): string | null {
const npub = getUserNpub(config);
if (!npub) return null;
return npub.slice(-7);
}
export async function startDaemonProcess(): Promise<void> {
const config = await loadConfig();
await startDaemon({
port: String(config.port || 8008),
host: config.host || undefined,
provider: config.provider || undefined,
});
}
export async function ensureDaemonRunning(): Promise<void> {
if (await isDaemonRunning()) {
return;
}
const config = await loadConfig();
if (config.daemonUrl) {
throw new Error(`Daemon is not reachable at ${config.daemonUrl}`);
}
console.log("Starting daemon...");
await startDaemonProcess();
}
export async function handleDaemonCommand(
path: string,
options: { method?: "GET" | "POST"; body?: object } = {},
): Promise<CommandResponse> {
try {
await ensureDaemonRunning();
const result = await callDaemon(path, options);
if (result.error) {
console.log(result.error);
process.exit(1);
}
if (result.output !== undefined) {
if (typeof result.output === "string") {
console.log(result.output);
} else {
try {
const formatted = JSON.stringify(result.output, null, 2);
console.log(formatted ?? String(result.output));
} catch {
console.log(String(result.output));
}
}
}
return result;
} catch (error) {
const message = (error as Error).message;
if (
message?.includes("fetch failed") ||
message?.includes("Connection refused")
) {
console.error("Daemon is not running and failed to auto-start");
process.exit(1);
}
console.error(message);
process.exit(1);
}
}