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
This commit is contained in:
redshift
2026-05-30 05:30:42 +00:00
parent 8db3b3cf8b
commit 94cd2ff15d
4 changed files with 64 additions and 16 deletions
+25 -8
View File
@@ -3,6 +3,7 @@ import { startDaemon } from "./start-daemon";
import {
handleDaemonCommand,
callDaemon,
callAuth,
ensureDaemonRunning,
isDaemonRunning,
loadConfig,
@@ -334,7 +335,8 @@ program
program
.command("remote <url>")
.description("Configure a remote daemon URL")
.action(async (url: string) => {
.option("--auth-url <authUrl>", "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<RoutstrdConfig> = { 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<string, string> = { 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",
+4 -3
View File
@@ -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<ClientEntry[]> {
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<void> {
export async function deleteClientAction(id: string): Promise<void> {
await ensureDaemonRunning();
const result = await callDaemon("/clients/delete", {
const result = await callAuth("/clients/delete", {
method: "POST",
body: { id },
});
+3
View File
@@ -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;
+32 -5
View File
@@ -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<CommandResponse> {
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<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();