Merge pull request #56 from Routstr/fix/bind-address-localhost

fix: bind to 127.0.0.1 by default instead of 0.0.0.0
This commit is contained in:
redshift
2026-07-28 20:25:01 +01:00
committed by GitHub
7 changed files with 175 additions and 27 deletions
+8
View File
@@ -74,6 +74,13 @@ With custom port:
routstrd start --port 9000
```
The daemon binds to `127.0.0.1` by default. To expose it on another interface:
```sh
routstrd start --host 0.0.0.0
```
Only expose the daemon behind appropriate network controls.
With specific provider:
```sh
routstrd start --provider https://your-provider.com
@@ -139,6 +146,7 @@ Configuration is stored in `~/.routstrd/config.json`:
```json
{
"port": 8008,
"host": "127.0.0.1",
"provider": null,
"cocodPath": null
}
+127 -11
View File
@@ -146,19 +146,29 @@ async function restartDaemonsAfterUpdate(): Promise<void> {
// (draining active connections) then exits.
await callDaemon("/stop", { method: "POST" });
for (let i = 0; i < 50; i++) {
// Wait for daemon to fully stop — both the HTTP health check to fail
// AND the legacy cocod pidfile to be released.
const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`;
for (let i = 0; i < 100; i++) {
await new Promise((resolve) => setTimeout(resolve, 100));
if (!(await isDaemonRunning())) break;
const healthDown = !(await isDaemonRunning());
const pidFileReleased = !existsSync(pidFilePath);
if (healthDown && pidFileReleased) break;
}
if (await isDaemonRunning()) {
throw new Error("routstrd did not stop within 5 seconds");
throw new Error("routstrd did not stop within 10 seconds");
}
console.log("routstrd daemon stopped.");
// Stop a legacy cocod daemon (from older routstrd versions) before
// starting the new in-process coco wallet.
await stopLegacyCocod();
console.log("Starting routstrd daemon...");
await startDaemon({
port: String(config.port || 8008),
host: config.host || undefined,
provider: config.provider || undefined,
});
console.log("routstrd daemon restarted.");
@@ -229,7 +239,7 @@ async function initDaemon(): Promise<void> {
console.log(`Database will be stored at: ${DB_PATH}`);
initializeWallet();
await startDaemon({ port: String(config.port || 8008) });
await startDaemon({ port: String(config.port || 8008), host: config.host || undefined });
await setupIntegration(config);
@@ -488,8 +498,9 @@ program
.command("start")
.description("Start the background daemon")
.option("--port <port>", "Port to listen on")
.option("--host <host>", "Bind address (default: 127.0.0.1)")
.option("-p, --provider <provider>", "Default provider to use")
.action(async (options: { port?: string; provider?: string }) => {
.action(async (options: { port?: string; host?: string; provider?: string }) => {
await requireLocalDaemon();
const config = await loadConfig();
// Stop a legacy cocod daemon (from older routstrd versions) before
@@ -497,6 +508,7 @@ program
await stopLegacyCocod();
await startDaemon({
port: options.port || String(config.port || 8008),
host: options.host || config.host || undefined,
provider: options.provider,
});
});
@@ -536,9 +548,97 @@ program
program
.command("balance")
.description("Get wallet and API key balances")
.action(async () => {
.option("--api-keys", "List all stored API keys (baseUrl + key + balance)", false)
.option(
"--delete-api-keys <baseUrl>",
"Delete the API key stored for the given provider base URL (refunds balance first)",
)
.option(
"--mint-url <url>",
"Mint URL to refund the deleted API key balance to (defaults to first mint in wallet)",
)
.action(async (options: { apiKeys: boolean; deleteApiKeys?: string; mintUrl?: string }) => {
await ensureDaemonRunning();
// --delete-api-keys <baseUrl>: refund then remove the API key for a
// single provider.
if (options.deleteApiKeys) {
const baseUrl = options.deleteApiKeys;
const queryParts = [`baseUrl=${encodeURIComponent(baseUrl)}`];
if (options.mintUrl) {
queryParts.push(`mintUrl=${encodeURIComponent(options.mintUrl)}`);
}
const result = await callDaemon(
`/keys/api/delete?${queryParts.join("&")}`,
{ method: "DELETE" },
);
if (result.error) {
console.log(result.error);
process.exit(1);
}
const out = result.output as
| {
baseUrl?: string;
removed?: boolean;
refunded?: boolean;
refundedAmount?: number;
refundMessage?: string;
message?: string;
}
| undefined;
if (out) {
console.log(out.message ?? `Removed API key for ${baseUrl}`);
if (out.refunded && out.refundedAmount !== undefined) {
console.log(` Refunded: ${out.refundedAmount} sats`);
} else if (out.refundMessage) {
console.log(` Refund: ${out.refundMessage}`);
}
}
return;
}
// --api-keys: list every stored API key with full details.
if (options.apiKeys) {
const result = await callDaemon("/keys/api");
if (result.error) {
console.log(result.error);
process.exit(1);
}
const data = result.output as
| {
apiKeys: Array<{
baseUrl: string;
key: string;
balance: number;
lastUsed: number | null;
}>;
count: number;
total: number;
unit: string;
}
| undefined;
console.log("=== API Keys ===\n");
if (!data || data.apiKeys.length === 0) {
console.log(" No API keys stored.");
return;
}
for (const k of data.apiKeys) {
const lastUsed = k.lastUsed
? new Date(k.lastUsed).toISOString()
: "never";
console.log(` ${k.baseUrl}`);
console.log(` key: ${k.key}`);
console.log(` balance: ${k.balance} ${data.unit}`);
console.log(` lastUsed: ${lastUsed}`);
console.log("");
}
console.log(` Total: ${data.apiKeys.length} key(s), ${data.total} ${data.unit}`);
return;
}
// Default: show the full wallet + API key balance summary.
const [walletResult, keysResult] = await Promise.all([
callDaemon("/balance"),
callDaemon("/keys/balance"),
@@ -1645,6 +1745,10 @@ serviceCmd
console.log("Starting routstrd via PM2...");
try {
// Stop a legacy cocod daemon (from older routstrd versions) before
// starting the new in-process coco wallet.
await stopLegacyCocod();
// Use --interpreter bun to ensure it runs with bun
execSync(`pm2 start "${daemonPath}" --name routstrd --interpreter bun`, {
stdio: "inherit",
@@ -1692,8 +1796,9 @@ program
.command("restart")
.description("Restart the background daemon")
.option("--port <port>", "Port to listen on")
.option("--host <host>", "Bind address (default: 127.0.0.1)")
.option("-p, --provider <provider>", "Default provider to use")
.action(async (options: { port?: string; provider?: string }) => {
.action(async (options: { port?: string; host?: string; provider?: string }) => {
await requireLocalDaemon();
const config = await loadConfig();
const wasRunning = await isDaemonRunning();
@@ -1702,16 +1807,21 @@ program
console.log("Stopping daemon...");
await callDaemon("/stop", { method: "POST" });
// Wait for daemon to fully stop
for (let i = 0; i < 50; i++) {
// Wait for daemon to fully stop — both the HTTP health check to fail
// AND the legacy cocod pidfile to be released (the daemon releases
// it during wallet disposal, which happens after server.close()).
const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`;
for (let i = 0; i < 100; i++) {
await new Promise((resolve) => setTimeout(resolve, 100));
if (!(await isDaemonRunning())) {
const healthDown = !(await isDaemonRunning());
const pidFileReleased = !existsSync(pidFilePath);
if (healthDown && pidFileReleased) {
break;
}
}
if (await isDaemonRunning()) {
logger.error("Daemon failed to stop within 5 seconds");
logger.error("Daemon failed to stop within 10 seconds");
process.exit(1);
}
console.log("Daemon stopped.");
@@ -1719,9 +1829,14 @@ program
console.log("Daemon was not running.");
}
// Stop a legacy cocod daemon (from older routstrd versions) before
// starting the new in-process coco wallet — both cannot share coco.db.
await stopLegacyCocod();
console.log("Starting daemon...");
await startDaemon({
port: options.port || String(config.port || 8008),
host: options.host || config.host || undefined,
provider: options.provider,
});
console.log("Daemon restarted.");
@@ -1804,6 +1919,7 @@ program
console.log("Starting daemon...");
await startDaemon({
port: String(config.port || 8008),
host: config.host || undefined,
provider: config.provider || undefined,
});
console.log(`Daemon restarted with mode '${selectedMode}'.`);
+8 -1
View File
@@ -1,8 +1,10 @@
export function parseArgs(argv: string[]): {
port: number;
host: string | null;
provider: string | null;
} {
const portFlagIndex = argv.findIndex((arg) => arg === "--port");
const hostFlagIndex = argv.findIndex((arg) => arg === "--host");
const providerFlagIndex = argv.findIndex(
(arg) => arg === "--provider" || arg === "-p",
);
@@ -11,9 +13,14 @@ export function parseArgs(argv: string[]): {
portFlagIndex !== -1
? Number.parseInt(argv[portFlagIndex + 1] || "8008", 10)
: 8008;
const hostValue =
hostFlagIndex !== -1 ? argv[hostFlagIndex + 1] : undefined;
const host = hostValue?.trim() || null;
const providerValue =
providerFlagIndex !== -1 ? argv[providerFlagIndex + 1] : undefined;
const provider = providerValue ? providerValue.trim() : null;
return { port, provider };
return { port, host, provider };
}
+4 -3
View File
@@ -65,6 +65,7 @@ async function main(): Promise<void> {
const config = await loadDaemonConfig();
const port = args.port;
const host = args.host || config.host || "127.0.0.1";
const provider = args.provider || config.provider;
const requestResponseLogDir =
process.env.ROUTSTRD_REQUEST_RESPONSE_LOG_DIR ||
@@ -80,7 +81,7 @@ async function main(): Promise<void> {
await ensureDirs();
const updatedConfig = { ...config, port, provider };
const updatedConfig = { ...config, port, host, provider };
saveDaemonConfig(updatedConfig);
const sqliteDriver = await createBunSqliteDriver(DB_PATH, { logger: daemonSdkLogger });
@@ -304,8 +305,8 @@ async function main(): Promise<void> {
process.once("SIGINT", shutdownForSignal);
process.once("SIGTERM", shutdownForSignal);
server.listen(port, async () => {
logger.log(`Routstr daemon listening on http://localhost:${port}/v1`);
server.listen(port, host, async () => {
logger.log(`Routstr daemon listening on http://${host}:${port}/v1`);
if (requestResponseLogDir) {
logger.log(`Raw request/response logs: ${requestResponseLogDir}`);
}
+20 -9
View File
@@ -27,11 +27,11 @@ function readDaemonOutput(offset: number): string {
}
}
async function isDaemonHealthy(port: string): Promise<boolean> {
async function isDaemonHealthy(port: string, host = "127.0.0.1"): Promise<boolean> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000);
try {
const existing = await fetch(`http://localhost:${port}/health`, {
const existing = await fetch(`http://${host}:${port}/health`, {
signal: controller.signal,
});
return existing.ok;
@@ -42,22 +42,31 @@ async function isDaemonHealthy(port: string): Promise<boolean> {
}
}
function clientHost(host?: string): string {
return !host || host === "0.0.0.0" ? "127.0.0.1" : host;
}
async function startDaemonUnlocked(
options: { port?: string; provider?: string },
options: { port?: string; host?: string; provider?: string },
): Promise<void> {
const args: string[] = [];
const port = options.port || "8008";
const host = options.host || "127.0.0.1";
const ch = clientHost(host);
const pollIntervalMs = 250;
const startupTimeoutMs = 10 * 60 * 1000;
if (await isDaemonHealthy(port)) {
console.log(`Routstr daemon already running on http://localhost:${port}/v1`);
if (await isDaemonHealthy(port, ch)) {
console.log(`Routstr daemon already running on http://${ch}:${port}/v1`);
return;
}
if (options.port) {
args.push("--port", options.port);
}
if (options.host) {
args.push("--host", options.host);
}
if (options.provider) {
args.push("--provider", options.provider);
}
@@ -98,7 +107,7 @@ async function startDaemonUnlocked(
);
}
if (await isDaemonHealthy(port)) {
if (await isDaemonHealthy(port, ch)) {
console.log(`Routstr daemon started (PID: ${proc.pid}).`);
return;
}
@@ -110,13 +119,15 @@ async function startDaemonUnlocked(
}
export async function startDaemon(
options: { port?: string; provider?: string } = {},
options: { port?: string; host?: string; provider?: string } = {},
): Promise<void> {
const port = options.port || "8008";
const host = options.host || "127.0.0.1";
const ch = clientHost(host);
const startupTimeoutMs = 10 * 60 * 1000;
if (await isDaemonHealthy(port)) {
console.log(`Routstr daemon already running on http://localhost:${port}/v1`);
if (await isDaemonHealthy(port, ch)) {
console.log(`Routstr daemon already running on http://${ch}:${port}/v1`);
return;
}
+2
View File
@@ -32,6 +32,7 @@ export interface NwcConfig {
export interface RoutstrdConfig {
port: number;
host: string;
provider: string | null;
cocodPath: string | null;
mode?: "xcashu" | "apikeys";
@@ -58,6 +59,7 @@ export interface RoutstrdConfig {
export const DEFAULT_CONFIG: RoutstrdConfig = {
port: 8008,
host: "127.0.0.1",
provider: null,
cocodPath: null,
mode: "apikeys",
+6 -3
View File
@@ -30,9 +30,11 @@ export async function loadConfig(): Promise<RoutstrdConfig> {
}
export function getDaemonBaseUrl(config: RoutstrdConfig): string {
return (
config.daemonUrl?.replace(/\/$/, "") || `http://localhost:${config.port}`
);
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 {
@@ -145,6 +147,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,
});
}