diff --git a/bun.lock b/bun.lock index 57b54b5..43913f1 100755 --- a/bun.lock +++ b/bun.lock @@ -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=="], } } diff --git a/package.json b/package.json index 2507c9e..d07b704 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/cli.ts b/src/cli.ts index be65236..1156db8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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 ") - .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 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 = { 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 ") + .description("Update the role of an existing npub (requires admin)") + .requiredOption("-r, --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 ") - .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); diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index f567e6e..b42acc6 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -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, }, }, }), diff --git a/src/tui/usage/data.ts b/src/tui/usage/data.ts index 8aade1f..cdcfa53 100644 --- a/src/tui/usage/data.ts +++ b/src/tui/usage/data.ts @@ -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 { 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( diff --git a/src/utils/clients.ts b/src/utils/clients.ts index 296bfb2..deee3f4 100644 --- a/src/utils/clients.ts +++ b/src/utils/clients.ts @@ -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 { - const config = await loadConfig(); const result = await callDaemon("/clients"); const clients = ( result.output as @@ -97,6 +66,7 @@ export async function getClientsList(): Promise { apiKey: string; createdAt: number; lastUsed?: number | null; + ownerNpub?: string; }>; } | undefined @@ -106,17 +76,14 @@ export async function getClientsList(): Promise { 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 { export async function deleteClientAction(id: string): Promise { 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 { 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 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 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 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`); diff --git a/src/utils/daemon-client.ts b/src/utils/daemon-client.ts index ba3fda2..0ed9b75 100644 --- a/src/utils/daemon-client.ts +++ b/src/utils/daemon-client.ts @@ -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 { const { method = "GET", body } = options; const config = await loadConfig();