Merge branch 'main' of https://github.com/Routstr/routstrd into feat/hermes-integration

This commit is contained in:
redshift
2026-05-04 15:55:52 +00:00
7 changed files with 134 additions and 118 deletions
+41 -20
View File
@@ -4,10 +4,11 @@
"": {
"dependencies": {
"@cashu/cashu-ts": "^3.1.1",
"@routstr/sdk": "^0.3.0",
"@routstr/sdk": "^0.3.2",
"applesauce-core": "^5.1.0",
"applesauce-relay": "^5.1.0",
"commander": "^14.0.2",
"nostr-tools": "^2.12.0",
"qrcode": "^1.5.4",
"rxjs": "^7.8.1",
"zustand": "^5.0.5",
@@ -24,19 +25,19 @@
"packages": {
"@cashu/cashu-ts": ["@cashu/cashu-ts@3.6.4", "", { "dependencies": { "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", "@scure/base": "^2.0.0", "@scure/bip32": "^2.0.1" } }, "sha512-a6Asqk+wPEk9a6BQmdLMeegngj0KHKicABUEtr1cUMLTAUYHVHfdfEuHDlU+bNUqXXyaTssG7yo/ZhfHrgbSkQ=="],
"@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="],
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
"@noble/curves": ["@noble/curves@2.2.0", "", { "dependencies": { "@noble/hashes": "2.2.0" } }, "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ=="],
"@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="],
"@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
"@routstr/sdk": ["@routstr/sdk@0.3.1", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "rxjs": "^7.8.1", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^11.7.2" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-sd4mOOPevoLUKmr3/GHVdcwLQi8PnddKkvGuQlJFTaKqQBoJYWFnc7+3EPZZmMl0wq03rbk92YV0ZMP0/5Zd7g=="],
"@routstr/sdk": ["@routstr/sdk@0.3.2", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "rxjs": "^7.8.1", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^11.7.2" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-sNUa48UwNcoFRidfVLOFQ+VGX723G1In5L1nb2w1C45GeiofSTUj9v+eBJCEU12WpBafFahHYaqQaaTzZ8LWKw=="],
"@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="],
"@scure/base": ["@scure/base@2.0.0", "", {}, "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="],
"@scure/bip32": ["@scure/bip32@2.2.0", "", { "dependencies": { "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0" } }, "sha512-zFr7t2F+a9+5tB7QbarF2HQNYrgjCNaoLAupZdKkrFMYMozJf5zqH2WJCQibMzm1qQ0QogrxVGO3qXfQDYMaQg=="],
"@scure/bip32": ["@scure/bip32@2.0.1", "", { "dependencies": { "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA=="],
"@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="],
"@scure/bip39": ["@scure/bip39@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1", "@scure/base": "2.0.0" } }, "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg=="],
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
@@ -126,13 +127,13 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@5.1.9", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw=="],
"nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="],
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
"node-abi": ["node-abi@3.89.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="],
"node-abi": ["node-abi@3.90.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-pZNQT7UnYlMwMBy5N1lV5X/YLTbZM5ncytN3xL7CHEzhDN8uVe0u55yaPUJICIJjaCW8NrM5BFdqr7HLweStNA=="],
"nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="],
"nostr-tools": ["nostr-tools@2.23.3", "", { "dependencies": { "@noble/ciphers": "2.1.1", "@noble/curves": "2.0.1", "@noble/hashes": "2.0.1", "@scure/base": "2.0.0", "@scure/bip32": "2.0.1", "@scure/bip39": "2.0.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-AALyt9k8xPdF4UV2mlLJ2mgCn4kpTB0DZ8t2r6wjdUh6anfx2cTVBsHUlo9U0EY/cKC5wcNyiMAmRJV5OVEalA=="],
"nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="],
@@ -210,22 +211,42 @@
"zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="],
"@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
"@scure/bip39/@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
"applesauce-core/nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="],
"applesauce-relay/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
"nostr-tools/@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="],
"applesauce-relay/nostr-tools": ["nostr-tools@2.19.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-qVLfoTpZegNYRJo5j+Oi6RPu0AwLP6jcvzcB3ySMnIT5DrAGNXfs5HNBspB/2HiGfH3GY+v6yXkTtcKSBQZwSg=="],
"nostr-tools/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
"applesauce-core/nostr-tools/@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="],
"nostr-tools/@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
"applesauce-core/nostr-tools/@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="],
"nostr-tools/@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="],
"applesauce-core/nostr-tools/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
"nostr-tools/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
"applesauce-core/nostr-tools/@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
"nostr-tools/@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="],
"applesauce-core/nostr-tools/@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="],
"applesauce-core/nostr-tools/@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="],
"applesauce-relay/nostr-tools/@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="],
"applesauce-relay/nostr-tools/@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="],
"applesauce-relay/nostr-tools/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
"applesauce-relay/nostr-tools/@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
"applesauce-relay/nostr-tools/@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="],
"applesauce-relay/nostr-tools/@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="],
"applesauce-core/nostr-tools/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
"applesauce-core/nostr-tools/@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="],
"applesauce-relay/nostr-tools/@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
"applesauce-relay/nostr-tools/@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="],
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "routstrd",
"version": "0.2.12",
"version": "0.2.15",
"module": "src/index.ts",
"type": "module",
"private": false,
@@ -24,7 +24,7 @@
},
"dependencies": {
"@cashu/cashu-ts": "^3.1.1",
"@routstr/sdk": "^0.3.0",
"@routstr/sdk": "^0.3.2",
"applesauce-core": "^5.1.0",
"applesauce-relay": "^5.1.0",
"commander": "^14.0.2",
+63 -19
View File
@@ -7,7 +7,6 @@ import {
isDaemonRunning,
loadConfig,
getDaemonBaseUrl,
getNpubSuffix,
getUserNpub,
} from "./utils/daemon-client";
import {
@@ -833,14 +832,19 @@ clientsCmd
},
);
// Npubs - manage admin npubs
// Npubs - manage npubs (admin and user roles)
const npubsCmd = program
.command("npubs")
.description("Manage admin npubs on the daemon");
.description("Manage npubs on the daemon (admin or user roles)");
type NpubEntry = {
npub: string;
role: string;
};
npubsCmd
.command("list")
.description("List configured admin npubs")
.description("List configured npubs with their roles")
.action(async () => {
await ensureDaemonRunning();
const config = await loadConfig();
@@ -851,20 +855,20 @@ npubsCmd
process.exit(1);
}
// Handle both wrapped { output: { npubs } } and direct { npubs } response formats
const data = (result.output as { npubs?: string[] } | undefined)?.npubs
const data = (result.output as { npubs?: NpubEntry[] } | undefined)?.npubs
? result.output
: result;
const npubs = (data as { npubs?: string[] } | undefined)?.npubs ?? [];
const npubs = (data as { npubs?: NpubEntry[] } | undefined)?.npubs ?? [];
if (npubs.length === 0) {
console.log("No admin npubs configured. Run 'routstrd npubs register' to register yourself as the first admin.");
return;
}
console.log(`Admin npubs (${npubs.length}):`);
console.log(`Npubs (${npubs.length}):`);
let found = false;
for (const npub of npubs) {
const marker = npub === userNpub ? " → you" : "";
if (npub === userNpub) found = true;
console.log(`- ${npub}${marker}`);
for (const entry of npubs) {
const marker = entry.npub === userNpub ? " → you" : "";
if (entry.npub === userNpub) found = true;
console.log(`- ${entry.npub} [${entry.role}]${marker}`);
}
if (userNpub && !found) {
console.log("");
@@ -893,10 +897,10 @@ npubsCmd
console.log(result.error);
process.exit(1);
}
const data = (result.output as { npubs?: string[] } | undefined)?.npubs
const data = (result.output as { npubs?: NpubEntry[] } | undefined)?.npubs
? result.output
: result;
const npubs = (data as { npubs?: string[] } | undefined)?.npubs ?? [];
const npubs = (data as { npubs?: NpubEntry[] } | undefined)?.npubs ?? [];
if (npubs.length > 0) {
console.log(`Admin npubs already configured (${npubs.length}). Ask your admin to add your npub. \n Your npub: ${userNpub}`);
return;
@@ -926,35 +930,75 @@ npubsCmd
npubsCmd
.command("add <npub>")
.description("Add an admin npub (hex pubkey or npub1...)")
.action(async (npubArg: string) => {
.description("Add a npub (hex pubkey or npub1...). Defaults to 'user' role unless --role is specified.")
.option("-r, --role <role>", "Role for the npub: 'admin' or 'user' (default: 'user')", "user")
.action(async (npubArg: string, options: { role: string }) => {
await ensureDaemonRunning();
const normalized = normalizeNostrPubkey(npubArg);
if (!normalized) {
console.error("Invalid npub value. Use npub1... or 64-char hex pubkey.");
process.exit(1);
}
if (options.role !== "admin" && options.role !== "user") {
console.error("Invalid role. Expected 'admin' or 'user'.");
process.exit(1);
}
const body: Record<string, string> = { npub: npubFromPubkey(normalized), role: options.role };
const result = await callDaemon("/npubs", {
method: "POST",
body: { npub: npubFromPubkey(normalized) },
body,
});
if (result.error) {
console.log(result.error);
process.exit(1);
}
const output = result.output as
| { npub?: string; added?: boolean; error?: string }
| { npub?: string; role?: string; added?: boolean; error?: string }
| undefined;
if (output?.npub) {
console.log(
`${output.added ? "Added" : "Already configured"} admin npub: ${output.npub}`,
`${output.added ? "Added" : "Already configured"} npub: ${output.npub} [${output.role ?? "user"}]`,
);
}
});
npubsCmd
.command("update <npub>")
.description("Update the role of an existing npub (requires admin)")
.requiredOption("-r, --role <role>", "New role: 'admin' or 'user'")
.action(async (npubArg: string, options: { role: string }) => {
await ensureDaemonRunning();
const normalized = normalizeNostrPubkey(npubArg);
if (!normalized) {
console.error("Invalid npub value. Use npub1... or 64-char hex pubkey.");
process.exit(1);
}
if (options.role !== "admin" && options.role !== "user") {
console.error("Invalid role. Expected 'admin' or 'user'.");
process.exit(1);
}
const result = await callDaemon("/npubs", {
method: "PATCH",
body: { npub: npubFromPubkey(normalized), role: options.role },
});
if (result.error) {
console.log(result.error);
process.exit(1);
}
// PATCH /npubs returns { npub, pubkey, role } at the top level, not wrapped in { output }
const data = (result.output ?? result) as
| { npub?: string; pubkey?: string; role?: string; error?: string }
| undefined;
if (data?.npub) {
console.log(`Updated npub ${data.npub} role to '${data.role}'.`);
} else {
console.log("Npub not found or update failed.");
}
});
npubsCmd
.command("delete <npub>")
.description("Delete an admin npub (hex pubkey or npub1...)")
.description("Delete an npub (hex pubkey or npub1...)")
.action(async (npubArg: string) => {
await ensureDaemonRunning();
const normalized = normalizeNostrPubkey(npubArg);
+4
View File
@@ -713,6 +713,7 @@ export function createDaemonRequestHandler(deps: {
apiKey: c.apiKey,
createdAt: c.createdAt,
lastUsed: c.lastUsed,
ownerNpub: c.ownerNpub,
}));
res.writeHead(200, { "Content-Type": "application/json" });
@@ -737,6 +738,7 @@ export function createDaemonRequestHandler(deps: {
const body = bodyText ? JSON.parse(bodyText) : {};
const name = body.name as string | undefined;
const explicitId = body.id as string | undefined;
const ownerNpub = body.ownerNpub as string | undefined;
if (!name || typeof name !== "string" || name.trim() === "") {
res.writeHead(400, { "Content-Type": "application/json" });
@@ -800,6 +802,7 @@ export function createDaemonRequestHandler(deps: {
name: name.trim(),
apiKey,
createdAt: Date.now(),
...(ownerNpub && typeof ownerNpub === "string" ? { ownerNpub } : {}),
};
deps.store
@@ -821,6 +824,7 @@ export function createDaemonRequestHandler(deps: {
name: name.trim(),
apiKey,
createdAt: newClient.createdAt,
ownerNpub: newClient.ownerNpub,
},
},
}),
+4 -15
View File
@@ -1,5 +1,5 @@
import type { UsageTrackingEntry } from "../../daemon/types.ts";
import { callDaemon, getNpubSuffix, isDaemonRunning, loadConfig } from "../../utils/daemon-client.ts";
import { callDaemon, isDaemonRunning } from "../../utils/daemon-client.ts";
import type { ClientStats, DayStats, ModelStats, ProviderStats, UsageStats } from "./types.ts";
export interface BalanceKey {
@@ -82,21 +82,10 @@ export async function fetchUsage(limit = 10000): Promise<UsageStats | null> {
const result = await callDaemon(`/usage?limit=${limit}`);
if (result.error) return null;
// The daemon returns { output: [...] } where output is the entries array directly.
// In remote daemon mode, client IDs are suffixed with the last 7 chars of our npub.
// Fetch the full daemon result, but only display/aggregate entries for our clients.
// The auth proxy filters usage to the authenticated npub's clients and
// returns client IDs without the owner suffix.
const entries = result.output as UsageTrackingEntry[] | undefined;
const entriesArray = Array.isArray(entries) ? entries : [];
const suffix = getNpubSuffix(await loadConfig());
const suffixStr = suffix ? `-${suffix}` : null;
const visibleEntries = suffixStr
? entriesArray
.filter((entry) => entry.client?.endsWith(suffixStr))
.map((entry) => ({
...entry,
client: entry.client?.slice(0, -suffixStr.length),
}))
: entriesArray;
const visibleEntries = Array.isArray(entries) ? entries : [];
// Calculate totals from visible entries
const totals = visibleEntries.reduce(
+19 -61
View File
@@ -4,49 +4,16 @@ import {
getDaemonBaseUrl,
ensureDaemonRunning,
} from "./daemon-client";
import {
parseSecretKey,
npubFromSecretKey,
} from "./nip98";
import { type RoutstrdConfig } from "./config";
import { logger } from "./logger";
import { CLIENT_INTEGRATIONS, CLIENT_CONFIGS } from "../integrations/registry";
export function getNpubSuffix(config: RoutstrdConfig): string | null {
if (!config.daemonUrl || !config.nsec) return null;
try {
const secretKey = parseSecretKey(config.nsec);
const npub = npubFromSecretKey(secretKey);
return npub.slice(-7);
} catch {
return null;
}
}
/**
* Add suffix to a client ID.
*/
export function addSuffixToId(id: string, suffix: string): string {
return `${id}-${suffix}`;
}
/**
* Remove suffix from a client ID if present.
*/
export function removeSuffixFromId(id: string, suffix: string): string {
const suffixStr = `-${suffix}`;
if (id.endsWith(suffixStr)) {
return id.slice(0, -suffixStr.length);
}
return id;
}
export interface ClientEntry {
clientId: string;
name: string;
apiKey: string;
createdAt: number;
lastUsed?: number | null;
ownerNpub?: string;
}
export interface DaemonClient {
@@ -55,6 +22,7 @@ export interface DaemonClient {
apiKey: string;
createdAt: number;
lastUsed?: number | null;
ownerNpub?: string;
}
/**
@@ -71,12 +39,14 @@ export function getClientsFromStore(store: { getState(): any }): ClientEntry[] {
apiKey: string;
createdAt: number;
lastUsed?: number | null;
ownerNpub?: string;
}) => ({
clientId: c.clientId,
name: c.name,
apiKey: c.apiKey,
createdAt: c.createdAt,
lastUsed: c.lastUsed,
ownerNpub: c.ownerNpub,
}),
);
}
@@ -86,7 +56,6 @@ export function getClientsFromStore(store: { getState(): any }): ClientEntry[] {
* Use this when running remotely (CLI in remote mode).
*/
export async function getClientsList(): Promise<ClientEntry[]> {
const config = await loadConfig();
const result = await callDaemon("/clients");
const clients = (
result.output as
@@ -97,6 +66,7 @@ export async function getClientsList(): Promise<ClientEntry[]> {
apiKey: string;
createdAt: number;
lastUsed?: number | null;
ownerNpub?: string;
}>;
}
| undefined
@@ -106,17 +76,14 @@ export async function getClientsList(): Promise<ClientEntry[]> {
return [];
}
const suffix = config.daemonUrl ? getNpubSuffix(config) : null;
return clients
.filter((c) => !suffix || c.id.endsWith(`-${suffix}`))
.map((c) => ({
clientId: suffix ? removeSuffixFromId(c.id, suffix) : c.id,
name: c.name,
apiKey: c.apiKey,
createdAt: c.createdAt,
lastUsed: c.lastUsed,
}));
return clients.map((c) => ({
clientId: c.id,
name: c.name,
apiKey: c.apiKey,
createdAt: c.createdAt,
lastUsed: c.lastUsed,
ownerNpub: c.ownerNpub,
}));
}
export async function addDaemonClient(
@@ -135,18 +102,14 @@ export async function addDaemonClient(
apiKey: existing.apiKey,
createdAt: existing.createdAt,
lastUsed: existing.lastUsed,
ownerNpub: existing.ownerNpub,
};
return { client, created: false };
}
const config = await loadConfig();
const suffix = getNpubSuffix(config);
const clientId = suffix ? addSuffixToId(derivedId, suffix) : derivedId;
const result = await callDaemon("/clients/add", {
method: "POST",
body: { name, id: clientId },
body: { name, id: derivedId },
});
@@ -196,13 +159,9 @@ export async function listClientsAction(): Promise<void> {
export async function deleteClientAction(id: string): Promise<void> {
await ensureDaemonRunning();
const config = await loadConfig();
const suffix = getNpubSuffix(config);
const resolvedId = suffix ? addSuffixToId(id, suffix) : id;
const result = await callDaemon("/clients/delete", {
method: "POST",
body: { id: resolvedId },
body: { id },
});
if (result.error) {
@@ -233,7 +192,6 @@ export interface AddClientOptions {
export async function addClientAction(options: AddClientOptions): Promise<void> {
await ensureDaemonRunning();
const config = await loadConfig();
const suffix = getNpubSuffix(config);
const integrationKeys: string[] = [];
if (options.opencode) integrationKeys.push("opencode");
@@ -259,7 +217,7 @@ export async function addClientAction(options: AddClientOptions): Promise<void>
await integrationFn(config, client.apiKey, integrationConfig);
console.log(`\n ${integrationConfig.name}:`);
console.log(` Client ID: ${suffix ? removeSuffixFromId(client.id, suffix) : client.id}`);
console.log(` Client ID: ${client.id}`);
console.log(` API Key: ${client.apiKey}`);
} catch (error) {
logger.error(
@@ -286,7 +244,7 @@ export async function addClientAction(options: AddClientOptions): Promise<void>
if (!created) {
console.log(`Client '${options.name}' already exists.`);
console.log(`\n ID: ${suffix ? removeSuffixFromId(client.id, suffix) : client.id}`);
console.log(`\n ID: ${client.id}`);
console.log(` Name: ${client.name}`);
console.log(` API Key: ${client.apiKey}`);
return;
@@ -295,7 +253,7 @@ export async function addClientAction(options: AddClientOptions): Promise<void>
if (message) {
console.log(message);
}
console.log(`\n ID: ${suffix ? removeSuffixFromId(client.id, suffix) : client.id}`);
console.log(`\n ID: ${client.id}`);
console.log(` Name: ${client.name}`);
console.log(` API Key: ${client.apiKey}`);
console.log(`\n Access Routstr at: ${getDaemonBaseUrl(config)}/v1`);
+1 -1
View File
@@ -37,7 +37,7 @@ export function getDaemonBaseUrl(config: RoutstrdConfig): string {
export async function callDaemon(
path: string,
options: { method?: "GET" | "POST" | "DELETE"; body?: object } = {},
options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: object } = {},
): Promise<CommandResponse> {
const { method = "GET", body } = options;
const config = await loadConfig();