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 <npub>: POST /npubs (accepts hex pubkey or npub1...)
  - delete <npub>: 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
This commit is contained in:
redshift
2026-04-28 11:15:20 +05:30
parent 6461b0f84a
commit ebec737191
4 changed files with 119 additions and 2 deletions

4
.gitignore vendored
View File

@@ -30,3 +30,7 @@ temp/
.pi/
.worktrees/
.routstrd/
# Standalone prototype scripts
index.ts

View File

@@ -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 <npub>")
.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 <npub>")
.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")

View File

@@ -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<CommandResponse> {
const { method = "GET", body } = options;
const config = await loadConfig();

View File

@@ -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,