From ebec737191761222332f74f1afdcbd87400c39e6 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:15:20 +0530 Subject: [PATCH] feat(cli): add npubs management commands - Add normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey to nip98.ts - Extend callDaemon to accept DELETE method - Add subcommands: - list: GET /npubs - add : POST /npubs (accepts hex pubkey or npub1...) - delete : DELETE /npubs/:npub (accepts hex pubkey or npub1...) - All npubs commands go through callDaemon, so they work with remote daemons and are automatically NIP-98 signed --- .gitignore | 4 ++ src/cli.ts | 83 ++++++++++++++++++++++++++++++++++++++ src/utils/daemon-client.ts | 2 +- src/utils/nip98.ts | 32 ++++++++++++++- 4 files changed, 119 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 4f96b19..345bc6a 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ temp/ .pi/ .worktrees/ +.routstrd/ + +# Standalone prototype scripts +index.ts diff --git a/src/cli.ts b/src/cli.ts index 7b0f7b9..6b2f534 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -27,6 +27,10 @@ import { import { createSdkStore } from "@routstr/sdk"; import { createBunSqliteDriver } from "@routstr/sdk/storage"; import * as QRCode from "qrcode"; +import { + normalizeNostrPubkey, + npubFromPubkey, +} from "./utils/nip98"; import { isCocodInstalled, resolveCocodExecutable, @@ -866,6 +870,85 @@ clientsCmd } }); +// Npubs - manage admin npubs +const npubsCmd = program + .command("npubs") + .description("Manage admin npubs on the daemon"); + +npubsCmd + .command("list") + .description("List configured admin npubs") + .action(async () => { + await ensureDaemonRunning(); + const result = await callDaemon("/npubs"); + if (result.error) { + console.log(result.error); + process.exit(1); + } + const npubs = (result.output as { npubs?: string[] } | undefined)?.npubs ?? []; + if (npubs.length === 0) { + console.log("No admin npubs configured."); + return; + } + console.log(`Admin npubs (${npubs.length}):`); + for (const npub of npubs) { + console.log(`- ${npub}`); + } + }); + +npubsCmd + .command("add ") + .description("Add an admin npub (hex pubkey or npub1...)") + .action(async (npubArg: string) => { + await ensureDaemonRunning(); + const normalized = normalizeNostrPubkey(npubArg); + if (!normalized) { + console.error("Invalid npub value. Use npub1... or 64-char hex pubkey."); + process.exit(1); + } + const result = await callDaemon("/npubs", { + method: "POST", + body: { npub: npubFromPubkey(normalized) }, + }); + if (result.error) { + console.log(result.error); + process.exit(1); + } + const output = result.output as + | { npub?: string; added?: boolean; error?: string } + | undefined; + if (output?.npub) { + console.log( + `${output.added ? "Added" : "Already configured"} admin npub: ${output.npub}`, + ); + } + }); + +npubsCmd + .command("delete ") + .description("Delete an admin npub (hex pubkey or npub1...)") + .action(async (npubArg: string) => { + await ensureDaemonRunning(); + const normalized = normalizeNostrPubkey(npubArg); + if (!normalized) { + console.error("Invalid npub value. Use npub1... or 64-char hex pubkey."); + process.exit(1); + } + const result = await callDaemon(`/npubs/${encodeURIComponent(npubFromPubkey(normalized))}`, { + method: "DELETE", + }); + if (result.error) { + console.log(result.error); + process.exit(1); + } + const output = result.output as + | { removed?: boolean; error?: string } + | undefined; + console.log( + output?.removed ? "Removed admin npub." : "Admin npub was not configured.", + ); + }); + // Monitor - interactive TUI program .command("monitor") diff --git a/src/utils/daemon-client.ts b/src/utils/daemon-client.ts index aeb9826..0fb1eec 100644 --- a/src/utils/daemon-client.ts +++ b/src/utils/daemon-client.ts @@ -34,7 +34,7 @@ export function getDaemonBaseUrl(config: RoutstrdConfig): string { export async function callDaemon( path: string, - options: { method?: "GET" | "POST"; body?: object } = {}, + options: { method?: "GET" | "POST" | "DELETE"; body?: object } = {}, ): Promise { const { method = "GET", body } = options; const config = await loadConfig(); diff --git a/src/utils/nip98.ts b/src/utils/nip98.ts index e737150..24b5a17 100644 --- a/src/utils/nip98.ts +++ b/src/utils/nip98.ts @@ -1,4 +1,4 @@ -import { finalizeEvent, nip19, type EventTemplate } from "nostr-tools"; +import { finalizeEvent, getPublicKey, nip19, type EventTemplate } from "nostr-tools"; const NIP98_KIND = 27235; @@ -45,6 +45,36 @@ function base64EncodeUtf8(value: string): string { return btoa(String.fromCharCode(...new TextEncoder().encode(value))); } +export function normalizeNostrPubkey(value: string): string | null { + const trimmed = value.trim(); + if (!trimmed) return null; + + if (/^[a-f0-9]{64}$/i.test(trimmed)) { + return trimmed.toLowerCase(); + } + + if (trimmed.toLowerCase().startsWith("npub1")) { + try { + const decoded = nip19.decode(trimmed); + if (decoded.type === "npub" && typeof decoded.data === "string") { + return decoded.data.toLowerCase(); + } + } catch { + return null; + } + } + + return null; +} + +export function npubFromPubkey(pubkey: string): string { + return nip19.npubEncode(pubkey.toLowerCase()); +} + +export function npubFromSecretKey(secretKey: Uint8Array): string { + return npubFromPubkey(getPublicKey(secretKey)); +} + export async function createNIP98Authorization( secretKey: Uint8Array, url: string,