From 94cd2ff15d528fae0857b7842a156c0af715a495 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sat, 30 May 2026 05:30:42 +0000 Subject: [PATCH] feat: add authUrl config for routing management commands to auth proxy Adds an authUrl field to RoutstrdConfig so CLI management commands (npubs, clients, usage) can be directed to the routstrd-auth proxy instead of the daemon. This fixes 'routstrd npubs list' failing with 'Only POST is supported' when the auth proxy and daemon run on separate ports (e.g., Cloudron setup: proxy on 8008, daemon on 8009). Changes: - config.ts: add authUrl to RoutstrdConfig - daemon-client.ts: add getAuthBaseUrl(), callAuth(), refactor callDaemon - cli.ts: npubs/usage commands use callAuth, remote command accepts --auth-url - clients.ts: clients commands use callAuth --- src/cli.ts | 33 +++++++++++++++++++++++++-------- src/utils/clients.ts | 7 ++++--- src/utils/config.ts | 3 +++ src/utils/daemon-client.ts | 37 ++++++++++++++++++++++++++++++++----- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index d37d35f..f009079 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,6 +3,7 @@ import { startDaemon } from "./start-daemon"; import { handleDaemonCommand, callDaemon, + callAuth, ensureDaemonRunning, isDaemonRunning, loadConfig, @@ -334,7 +335,8 @@ program program .command("remote ") .description("Configure a remote daemon URL") - .action(async (url: string) => { + .option("--auth-url ", "URL of the auth proxy for management commands (npubs, clients, usage)") + .action(async (url: string, options: { authUrl?: string }) => { try { new URL(url); } catch { @@ -342,12 +344,24 @@ program process.exit(1); } + if (options.authUrl) { + try { + new URL(options.authUrl); + } catch { + console.error(`Invalid auth URL: ${options.authUrl}`); + process.exit(1); + } + } + if (!existsSync(CONFIG_DIR)) { mkdirSync(CONFIG_DIR, { recursive: true }); } const config = await loadConfig(); const updates: Partial = { daemonUrl: url }; + if (options.authUrl) { + updates.authUrl = options.authUrl; + } let generatedNpub: string | undefined; if (!config.nsec) { @@ -366,6 +380,9 @@ program await Bun.write(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2)); console.log(`Remote daemon URL set to: ${url}`); + if (options.authUrl) { + console.log(`Auth proxy URL set to: ${options.authUrl}`); + } if (generatedNpub) { console.log( `\nA new Nostr identity has been generated for remote authentication.`, @@ -666,7 +683,7 @@ program ? Math.min(requested, 1000) : 10; - const result = await callDaemon(`/usage?limit=${limit}`); + const result = await callAuth(`/usage?limit=${limit}`); if (result.error) { console.log(result.error); process.exit(1); @@ -882,7 +899,7 @@ npubsCmd await ensureDaemonRunning(); const config = await loadConfig(); const userNpub = getUserNpub(config); - const result = await callDaemon("/npubs"); + const result = await callAuth("/npubs"); if (result.error) { console.log(result.error); process.exit(1); @@ -925,7 +942,7 @@ npubsCmd ); process.exit(1); } - const result = await callDaemon("/npubs"); + const result = await callAuth("/npubs"); if (result.error) { console.log(result.error); process.exit(1); @@ -943,7 +960,7 @@ npubsCmd console.error("Failed to normalize user npub."); process.exit(1); } - const addResult = await callDaemon("/npubs", { + const addResult = await callAuth("/npubs", { method: "POST", body: { npub: npubFromPubkey(normalized) }, }); @@ -977,7 +994,7 @@ npubsCmd process.exit(1); } const body: Record = { npub: npubFromPubkey(normalized), role: options.role }; - const result = await callDaemon("/npubs", { + const result = await callAuth("/npubs", { method: "POST", body, }); @@ -1010,7 +1027,7 @@ npubsCmd console.error("Invalid role. Expected 'admin' or 'user'."); process.exit(1); } - const result = await callDaemon("/npubs", { + const result = await callAuth("/npubs", { method: "PATCH", body: { npub: npubFromPubkey(normalized), role: options.role }, }); @@ -1039,7 +1056,7 @@ npubsCmd console.error("Invalid npub value. Use npub1... or 64-char hex pubkey."); process.exit(1); } - const result = await callDaemon( + const result = await callAuth( `/npubs/${encodeURIComponent(npubFromPubkey(normalized))}`, { method: "DELETE", diff --git a/src/utils/clients.ts b/src/utils/clients.ts index 4a8d78a..0fb722e 100644 --- a/src/utils/clients.ts +++ b/src/utils/clients.ts @@ -1,4 +1,5 @@ import { + callAuth, callDaemon, loadConfig, getDaemonBaseUrl, @@ -56,7 +57,7 @@ export function getClientsFromStore(store: { getState(): any }): ClientEntry[] { * Use this when running remotely (CLI in remote mode). */ export async function getClientsList(): Promise { - const result = await callDaemon("/clients"); + const result = await callAuth("/clients"); const clients = ( result.output as | { @@ -107,7 +108,7 @@ export async function addDaemonClient( return { client, created: false }; } - const result = await callDaemon("/clients/add", { + const result = await callAuth("/clients/add", { method: "POST", body: { name, id: derivedId }, }); @@ -159,7 +160,7 @@ export async function listClientsAction(): Promise { export async function deleteClientAction(id: string): Promise { await ensureDaemonRunning(); - const result = await callDaemon("/clients/delete", { + const result = await callAuth("/clients/delete", { method: "POST", body: { id }, }); diff --git a/src/utils/config.ts b/src/utils/config.ts index 253b3f9..a148ba5 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -35,6 +35,9 @@ export interface RoutstrdConfig { cocodPath: string | null; mode?: "xcashu" | "apikeys"; daemonUrl?: string; + /** URL of the auth proxy (routstrd-auth) for management endpoints (npubs, clients, usage). + * Defaults to daemonUrl or localhost:{port} if not set. */ + authUrl?: string; nsec?: string; /** Nostr hex pubkey for routstr review/model events (kind 38425/38423). */ routstrPubkey?: string; diff --git a/src/utils/daemon-client.ts b/src/utils/daemon-client.ts index 0ed9b75..1086072 100644 --- a/src/utils/daemon-client.ts +++ b/src/utils/daemon-client.ts @@ -35,13 +35,20 @@ export function getDaemonBaseUrl(config: RoutstrdConfig): string { ); } -export async function callDaemon( +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 } = {}, + options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: object }, + config: RoutstrdConfig, ): Promise { const { method = "GET", body } = options; - const config = await loadConfig(); - const baseUrl = getDaemonBaseUrl(config); const url = `${baseUrl}${path}`; const bodyString = body ? JSON.stringify(body) : undefined; @@ -50,7 +57,7 @@ export async function callDaemon( : undefined; let authorization: string | undefined; - if (config.daemonUrl && config.nsec) { + if ((config.daemonUrl || config.authUrl) && config.nsec) { const secretKey = parseSecretKey(config.nsec); authorization = await createNIP98Authorization( secretKey, @@ -77,6 +84,26 @@ export async function callDaemon( return response.json() as Promise; } +export async function callDaemon( + path: string, + options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: object } = {}, +): Promise { + 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 { + const config = await loadConfig(); + const baseUrl = getAuthBaseUrl(config); + return _callUrl(baseUrl, path, options, config); +} + export async function isDaemonRunning(): Promise { try { const config = await loadConfig();