lots of things that I forgot to commit 😂

This commit is contained in:
pablof7z 2023-07-06 22:29:31 +02:00
commit 6f16cafd41
13 changed files with 395 additions and 130 deletions

View file

@ -86,7 +86,11 @@ $ npm run nsecbunkerd start
## Testing with `nsecbunker-client`
nsecbunker ships with a simple client that can request signatures from an nsecbunkerd:
```
nsecbunker-client sign <target-npub> "hi, I'm signing from the command line with my nsecbunkerd!"
```
# Authors

122
package-lock.json generated
View file

@ -1,27 +1,27 @@
{
"name": "nsecbunkerd",
"version": "0.6.1",
"version": "0.6.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nsecbunkerd",
"version": "0.6.1",
"version": "0.6.5",
"license": "CC BY-NC-ND 4.0",
"dependencies": {
"@inquirer/password": "^1.1.2",
"@inquirer/prompts": "^1.2.3",
"@nostr-dev-kit/ndk": "^0.6.0",
"@prisma/client": "^4.16.1",
"@nostr-dev-kit/ndk": "^0.6.5",
"@prisma/client": "^4.16.2",
"@scure/base": "^1.1.1",
"@types/yargs": "^17.0.24",
"@typescript-eslint/eslint-plugin": "^5.60.0",
"@typescript-eslint/parser": "^5.60.0",
"dotenv": "^16.3.1",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eventemitter3": "^5.0.1",
"isomorphic-ws": "^5.0.0",
"websocket-polyfill": "^0.0.3",
"ws": "^8.13.0",
"yargs": "^17.7.2"
},
"bin": {
@ -31,7 +31,7 @@
"devDependencies": {
"@types/debug": "^4.1.8",
"@types/node": "^18.16.18",
"prisma": "^4.16.1",
"prisma": "^4.16.2",
"ts-node": "^10.9.1",
"typescript": "^5.1.3"
}
@ -961,9 +961,9 @@
}
},
"node_modules/@nostr-dev-kit/ndk": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.6.0.tgz",
"integrity": "sha512-0ptE6OIZhFW+aRRIXAI8PvUIoVU6iQLpiwFtJj48XAUO2EC3WiSuqKLshjg6wj1bbo9qGs1PyFS9AUhUlWWJtg==",
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.6.5.tgz",
"integrity": "sha512-D95sDEonyFJhdTM1YswPOAYKaScnOlvZcxoyt9SlXnibT+gC9AGLtwAmpziK9zPC2h5Hh2GX44BnYXUGolLjZw==",
"dependencies": {
"@noble/hashes": "^1.3.1",
"@noble/secp256k1": "^2.0.0",
@ -987,12 +987,12 @@
}
},
"node_modules/@prisma/client": {
"version": "4.16.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.1.tgz",
"integrity": "sha512-CoDHu7Bt+NuDo40ijoeHP79EHtECsPBTy3yte5Yo3op8TqXt/kV0OT5OrsWewKvQGKFMHhYQ+ePed3zzjYdGAw==",
"version": "4.16.2",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.2.tgz",
"integrity": "sha512-qCoEyxv1ZrQ4bKy39GnylE8Zq31IRmm8bNhNbZx7bF2cU5aiCCnSa93J2imF88MBjn7J9eUQneNxUQVJdl/rPQ==",
"hasInstallScript": true,
"dependencies": {
"@prisma/engines-version": "4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c"
"@prisma/engines-version": "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81"
},
"engines": {
"node": ">=14.17"
@ -1007,16 +1007,16 @@
}
},
"node_modules/@prisma/engines": {
"version": "4.16.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.16.1.tgz",
"integrity": "sha512-gpZG0kGGxfemgvK/LghHdBIz+crHkZjzszja94xp4oytpsXrgt/Ice82MvPsWMleVIniKuARrowtsIsim0PFJQ==",
"version": "4.16.2",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.16.2.tgz",
"integrity": "sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==",
"devOptional": true,
"hasInstallScript": true
},
"node_modules/@prisma/engines-version": {
"version": "4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c.tgz",
"integrity": "sha512-tMWAF/qF00fbUH1HB4Yjmz6bjh7fzkb7Y3NRoUfMlHu6V+O45MGvqwYxqwBjn1BIUXkl3r04W351D4qdJjrgvA=="
"version": "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz",
"integrity": "sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg=="
},
"node_modules/@scure/base": {
"version": "1.1.1",
@ -3159,6 +3159,14 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/isomorphic-ws": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/jest-diff": {
"version": "29.5.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz",
@ -4006,13 +4014,13 @@
}
},
"node_modules/prisma": {
"version": "4.16.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.1.tgz",
"integrity": "sha512-C2Xm7yxHxjFjjscBEW4tmoraPHH/Vyu/A0XABdbaFtoiOZARsxvOM7rwc2iZ0qVxbh0bGBGBWZUSXO/52/nHBQ==",
"version": "4.16.2",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.2.tgz",
"integrity": "sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/engines": "4.16.1"
"@prisma/engines": "4.16.2"
},
"bin": {
"prisma": "build/index.js",
@ -4937,6 +4945,26 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@ -5550,9 +5578,9 @@
}
},
"@nostr-dev-kit/ndk": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.6.0.tgz",
"integrity": "sha512-0ptE6OIZhFW+aRRIXAI8PvUIoVU6iQLpiwFtJj48XAUO2EC3WiSuqKLshjg6wj1bbo9qGs1PyFS9AUhUlWWJtg==",
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-0.6.5.tgz",
"integrity": "sha512-D95sDEonyFJhdTM1YswPOAYKaScnOlvZcxoyt9SlXnibT+gC9AGLtwAmpziK9zPC2h5Hh2GX44BnYXUGolLjZw==",
"requires": {
"@noble/hashes": "^1.3.1",
"@noble/secp256k1": "^2.0.0",
@ -5576,23 +5604,23 @@
}
},
"@prisma/client": {
"version": "4.16.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.1.tgz",
"integrity": "sha512-CoDHu7Bt+NuDo40ijoeHP79EHtECsPBTy3yte5Yo3op8TqXt/kV0OT5OrsWewKvQGKFMHhYQ+ePed3zzjYdGAw==",
"version": "4.16.2",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.16.2.tgz",
"integrity": "sha512-qCoEyxv1ZrQ4bKy39GnylE8Zq31IRmm8bNhNbZx7bF2cU5aiCCnSa93J2imF88MBjn7J9eUQneNxUQVJdl/rPQ==",
"requires": {
"@prisma/engines-version": "4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c"
"@prisma/engines-version": "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81"
}
},
"@prisma/engines": {
"version": "4.16.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.16.1.tgz",
"integrity": "sha512-gpZG0kGGxfemgvK/LghHdBIz+crHkZjzszja94xp4oytpsXrgt/Ice82MvPsWMleVIniKuARrowtsIsim0PFJQ==",
"version": "4.16.2",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.16.2.tgz",
"integrity": "sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==",
"devOptional": true
},
"@prisma/engines-version": {
"version": "4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c.tgz",
"integrity": "sha512-tMWAF/qF00fbUH1HB4Yjmz6bjh7fzkb7Y3NRoUfMlHu6V+O45MGvqwYxqwBjn1BIUXkl3r04W351D4qdJjrgvA=="
"version": "4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz",
"integrity": "sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg=="
},
"@scure/base": {
"version": "1.1.1",
@ -7121,6 +7149,12 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"isomorphic-ws": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
"requires": {}
},
"jest-diff": {
"version": "29.5.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz",
@ -7722,12 +7756,12 @@
}
},
"prisma": {
"version": "4.16.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.1.tgz",
"integrity": "sha512-C2Xm7yxHxjFjjscBEW4tmoraPHH/Vyu/A0XABdbaFtoiOZARsxvOM7rwc2iZ0qVxbh0bGBGBWZUSXO/52/nHBQ==",
"version": "4.16.2",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-4.16.2.tgz",
"integrity": "sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==",
"devOptional": true,
"requires": {
"@prisma/engines": "4.16.1"
"@prisma/engines": "4.16.2"
}
},
"punycode": {
@ -8369,6 +8403,12 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"requires": {}
},
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "nsecbunkerd",
"version": "0.6.4",
"version": "0.7.1",
"description": "nsecbunker daemon",
"main": "dist/index.js",
"bin": {
@ -35,21 +35,23 @@
"dependencies": {
"@inquirer/password": "^1.1.2",
"@inquirer/prompts": "^1.2.3",
"@nostr-dev-kit/ndk": "^0.6.5",
"@prisma/client": "^4.16.1",
"@nostr-dev-kit/ndk": "^0.7.4",
"@prisma/client": "^4.16.2",
"@scure/base": "^1.1.1",
"@types/yargs": "^17.0.24",
"dotenv": "^16.3.1",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eventemitter3": "^5.0.1",
"isomorphic-ws": "^5.0.0",
"websocket-polyfill": "^0.0.3",
"ws": "^8.13.0",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/debug": "^4.1.8",
"@types/node": "^18.16.18",
"prisma": "^4.16.1",
"prisma": "^4.16.2",
"ts-node": "^10.9.1",
"typescript": "^5.1.3"
}

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "KeyUser" ADD COLUMN "revokedAt" DATETIME;

View file

@ -13,6 +13,7 @@ model KeyUser {
userPubkey String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
revokedAt DateTime?
lastUsedAt DateTime?
description String?
logs Log[]

View file

@ -1,52 +1,129 @@
import NDK, { NDKUser, NDKEvent, NDKPrivateKeySigner, NDKNip46Signer, NostrEvent } from '@nostr-dev-kit/ndk';
import fs from 'fs';
const remotePubkey = process.argv[2];
const content = process.argv[3];
const command = process.argv[2];
const remotePubkey = process.argv[3];
const content = process.argv[4];
const dontPublish = process.argv.includes('--dont-publish');
const debug = process.argv.includes('--debug');
if (!content) {
console.log('Usage: node src/client.js <remote-npub> <content>');
if (!command) {
console.log('Usage: node src/client.js <command> <remote-npub> <content> [--dont-publish] [--debug] [--pk <key>]');
console.log('');
console.log(`\t<command>: command to run (ping, sign)`);
console.log(`\t<remote-npub>: npub that should be published as`);
console.log(`\t<content>: event JSON to sign | or kind:1 content string to sign`);
console.log(`\t<content>: event JSON to sign (no need for pubkey or id fields) | or kind:1 content string to sign`);
console.log('\t--dont-publish: do not publish the event to the relay');
console.log('\t--debug: enable debug mode');
process.exit(1);
}
async function createNDK(): Promise<NDK> {
const ndk = new NDK({
explicitRelayUrls: ['wss://nostr.vulpem.com', "wss://67aee52897df.ngrok.app"],
explicitRelayUrls: ['wss://nostr.vulpem.com', "wss://relay.nsecbunker.com"],
});
ndk.pool.on('relay:connect', () => console.log('✅ connected'));
ndk.pool.on('relay:disconnect', () => console.log('❌ disconnected'));
if (debug) {
ndk.pool.on('connect', () => console.log('✅ connected'));
ndk.pool.on('disconnect', () => console.log('❌ disconnected'));
}
await ndk.connect(5000);
return ndk;
}
// switch (command) {
// case 'ping':
// ping(remotePubkey);
function getPrivateKeyPath() {
const home = process.env.HOME || process.env.USERPROFILE;
return `${home}/.nsecbunker-client-private.key`;
}
function savePrivateKey(pk: string) {
const path = getPrivateKeyPath();
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
fs.writeFileSync(`${path}/private.key`, pk);
}
function loadPrivateKey(): string | undefined {
const path = getPrivateKeyPath();
if (!fs.existsSync(path)) {
return undefined;
}
return fs.readFileSync(`${path}/private.key`).toString();
}
(async () => {
const remoteUser = new NDKUser({npub: remotePubkey});
const ndk = await createNDK();
const localSigner = NDKPrivateKeySigner.generate();
// const localSigner = new NDKPrivateKeySigner('b8baad35c387d7cf84d52e0958d9a02aff214393a85b0703de4146c7a3697bb3');
const pk = loadPrivateKey();
let localSigner: NDKPrivateKeySigner;
if (pk) {
localSigner = new NDKPrivateKeySigner(pk);
} else {
localSigner = NDKPrivateKeySigner.generate();
savePrivateKey(localSigner.privateKey!);
}
const signer = new NDKNip46Signer(ndk, remoteUser.hexpubkey(), localSigner);
console.log(`local pubkey`, (await signer.user()).npub);
console.log(`remote pubkey`, remotePubkey);
if (debug) console.log(`local pubkey`, (await localSigner.user()).npub);
if (debug) console.log(`remote pubkey`, remotePubkey);
ndk.signer = signer;
setTimeout(async () => {
try {
if (debug) console.log(`waiting for authorization (check your nsecBunker)...`);
await signer.blockUntilReady();
console.log(`authorized to sign as`, remotePubkey);
} catch(e) {
console.log('error:', e);
process.exit(1);
}
if (debug) console.log(`authorized to sign as`, remotePubkey);
const event = new NDKEvent(ndk, {
pubkey: remoteUser.hexpubkey(),
let event;
try {
const json = JSON.parse(content);
event = new NDKEvent(ndk, json);
if (!event.tags) { event.tags = []; }
if (!event.content) { event.content = ""; }
if (!event.kind) { throw "No kind on the event to sign!"; }
} catch (e) {
event = new NDKEvent(ndk, {
kind: 1,
content,
tags: [
['client', 'nsecbunker-client']
],
} as NostrEvent);
}
event.pubkey = remoteUser.hexpubkey();
try {
await event.sign();
console.log('resulting event', JSON.stringify(await event.toNostrEvent()));
if (debug) {
console.log({
event: event.rawEvent(),
signature: event.sig,
});
} else {
console.log(event.sig);
}
if (!dontPublish) {
await event.publish();
}
process.exit(0);
} catch(e) {
console.log('sign error', e);
}
}, 2000);
})();

View file

@ -18,7 +18,7 @@ interface IOpts {
export async function start(opts: IOpts) {
const configData = await getCurrentConfig(opts.config);
if (opts.adminNpubs) {
if (opts.adminNpubs && opts.adminNpubs.length > 0) {
configData.admin.npubs = opts.adminNpubs;
}

View file

@ -93,7 +93,6 @@ async function setupSkeletonProfile(key: NDKPrivateKeySigner) {
['r', 'wss://relay.f7z.io'],
['r', 'wss://relay.snort.social'],
['r', 'wss://relay.damus.io'],
['r', 'wss://relay.damus.io'],
],
pubkey: user.hexpubkey(),
} as NostrEvent);

View file

@ -0,0 +1,24 @@
import { NDKRpcRequest } from "@nostr-dev-kit/ndk";
import AdminInterface from "../index.js";
import prisma from "../../../db.js";
export default async function revokeUser(admin: AdminInterface, req: NDKRpcRequest) {
const [ keyUserId ] = req.params as [ string ];
if (!keyUserId) throw new Error("Invalid params");
const keyUserIdInt = parseInt(keyUserId);
if (isNaN(keyUserIdInt)) throw new Error("Invalid params");
await prisma.keyUser.update({
where: {
id: keyUserIdInt,
},
data: {
revokedAt: new Date(),
}
});
const result = JSON.stringify(["ok"]);
return admin.rpc.sendResponse(req.id, req.pubkey, result, 24134);
}

View file

@ -1,4 +1,4 @@
import NDK, { NDKPrivateKeySigner, NDKRpcRequest, NDKRpcResponse, NDKUser } from '@nostr-dev-kit/ndk';
import NDK, { NDKPrivateKeySigner, NDKRpcRequest, NDKRpcResponse, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk';
import { NDKNostrRpc } from '@nostr-dev-kit/ndk';
import { debug } from 'debug';
import { Key, KeyUser } from '../run';
@ -8,7 +8,10 @@ import createNewKey from './commands/create_new_key';
import createNewPolicy from './commands/create_new_policy';
import createNewToken from './commands/create_new_token';
import unlockKey from './commands/unlock_key';
import revokeUser from './commands/revoke_user';
import fs from 'fs';
import { validateRequestFromAdmin } from './validations/request-from-admin';
import { dmUser } from '../../utils/dm-user';
export type IAdminOpts = {
npubs: string[];
@ -43,7 +46,7 @@ class AdminInterface {
let connectionString = `bunker://${user.npub}`;
if (opts.adminRelays.length > 0) {
connectionString += `@${opts.adminRelays.join(',').replace(/wss:\/\//g, '')}`;
connectionString += '@' + encodeURIComponent(`${opts.adminRelays.join(',').replace(/wss:\/\//g, '')}`);
}
console.log(`\n\nnsecBunker connection string:\n\n${connectionString}\n\n`);
@ -54,11 +57,25 @@ class AdminInterface {
this.signerUser = user;
this.connect();
this.notifyAdminsOfNewConnection(connectionString);
});
this.rpc = new NDKNostrRpc(this.ndk, this.ndk.signer!, debug("ndk:rpc"));
}
private async notifyAdminsOfNewConnection(connectionString: string) {
const blastrNdk = new NDK({
explicitRelayUrls: ['wss://blastr.f7z.xyz', 'wss://nostr.mutinywallet.com'],
signer: this.ndk.signer
});
await blastrNdk.connect(2500);
for (const npub of this.npubs) {
dmUser(blastrNdk, npub, `nsecBunker has started; use ${connectionString} to connect to it and unlock your key(s)`);
}
}
/**
* Get the npub of the admin interface.
*/
@ -75,10 +92,10 @@ class AdminInterface {
this.ndk.pool.on('relay:connect', () => console.log('✅ nsecBunker Admin Interface ready'));
this.ndk.pool.on('relay:disconnect', () => console.log('❌ admin disconnected'));
this.ndk.connect(2500).then(() => {
// connect for whitelisted admins
this.rpc.subscribe({
"kinds": [24134 as number], // 24134
"#p": [this.signerUser!.hexpubkey()],
"authors": this.npubs.map((npub) => (new NDKUser({npub}).hexpubkey())),
});
this.rpc.on('request', (req) => this.handleRequest(req));
@ -89,30 +106,38 @@ class AdminInterface {
}
private async handleRequest(req: NDKRpcRequest) {
// await this.validateRequest(req);
try {
await this.validateRequest(req);
switch (req.method) {
case 'get_keys': this.reqGetKeys(req); break;
case 'get_key_users': this.reqGetKeyUsers(req); break;
case 'get_key_tokens': this.reqGetKeyTokens(req); break;
case 'create_new_key': createNewKey(this, req); break;
case 'unlock_key': unlockKey(this, req); break;
case 'create_new_policy': createNewPolicy(this, req); break;
case 'get_policies': this.reqListPolicies(req); break;
case 'create_new_token': createNewToken(this, req); break;
case 'get_keys': await this.reqGetKeys(req); break;
case 'get_key_users': await this.reqGetKeyUsers(req); break;
case 'get_key_tokens': await this.reqGetKeyTokens(req); break;
case 'revoke_user': await revokeUser(this, req); break;
case 'create_new_key': await createNewKey(this, req); break;
case 'unlock_key': await unlockKey(this, req); break;
case 'create_new_policy': await createNewPolicy(this, req); break;
case 'get_policies': await this.reqListPolicies(req); break;
case 'create_new_token': await createNewToken(this, req); break;
default:
console.log(`Unknown method ${req.method}`);
return this.rpc.sendResponse(
req.id,
req.pubkey,
JSON.stringify(['error', `Unknown method ${req.method}`]),
24134
);
}
} catch (err: any) {
console.error(`Error handling request ${req.method}: ${err.message}`, req.params);
return this.rpc.sendResponse(req.id, req.pubkey, JSON.stringify(['error', err?.message]), 24134);
}
}
private async validateRequest(req: NDKRpcRequest) {
// TODO validate pubkey, validate signature
private async validateRequest(req: NDKRpcRequest): Promise<void> {
if (!await validateRequestFromAdmin(req, this.npubs)) {
throw new Error('You are not designated to administrate this bunker');
}
}
/**
@ -220,13 +245,15 @@ class AdminInterface {
/**
* This function is called when a request is received from a remote user that needs
* to be approved by the admin interface.
*
* @returns true if the request is approved, false if it is denied, undefined if it timedout
*/
public async requestPermission(
keyName: string,
remotePubkey: string,
method: string,
param: any
): Promise<boolean> {
): Promise<boolean | undefined> {
const keyUser = await prisma.keyUser.findUnique({
where: {
unique_key_user: {
@ -254,21 +281,52 @@ class AdminInterface {
console.log(`param`, param);
console.log(`keyUser`, keyUser);
/**
* If an admin doesn't respond within 10 seconds, report back to the user that the request timed out
*/
setTimeout(() => {
resolve(undefined);
}, 10000);
for (const npub of this.npubs) {
const remoteUser = new NDKUser({npub});
console.log(`sending request to ${npub}`, remoteUser.hexpubkey());
this.rpc.sendRequest(
remoteUser.hexpubkey(),
'acl',
[JSON.stringify({
const params = JSON.stringify({
keyName,
remotePubkey,
method,
param,
description: keyUser?.description,
})],
24134, // 24134
});
this.rpc.sendRequest(
remoteUser.hexpubkey(),
'acl',
[params],
24134,
(res: NDKRpcResponse) => {
this.requestPermissionResponse(
remotePubkey,
keyName,
method,
param,
resolve,
res
);
}
);
}
});
}
public async requestPermissionResponse(
remotePubkey: string,
keyName: string,
method: string,
param: string,
resolve: (value: boolean) => void,
res: NDKRpcResponse
) {
let resObj;
try {
resObj = JSON.parse(res.result);
@ -277,8 +335,6 @@ class AdminInterface {
return;
}
console.log('request result', resObj);
switch (resObj[0]) {
case 'always': {
allowAllRequestsFromKey(
@ -288,19 +344,18 @@ class AdminInterface {
param,
resObj[1],
resObj[2]
).then(() => {
);
resolve(true);
});
break;
}
case 'never': {
console.log('not implemented');
break;
}
default:
console.log('request result', res.result);
}
}
);
}
});
}
}
export default AdminInterface;

View file

@ -0,0 +1,18 @@
import { NDKRpcRequest } from "@nostr-dev-kit/ndk";
import { nip19 } from "nostr-tools";
export async function validateRequestFromAdmin(
req: NDKRpcRequest,
npubs: string[],
): Promise<boolean> {
const hexpubkey = req.pubkey;
if (!hexpubkey) {
console.log('missing pubkey');
return false;
}
const hexpubkeys = npubs.map((npub) => nip19.decode(npub).data as string);
return hexpubkeys.includes(hexpubkey);
}

View file

@ -70,11 +70,13 @@ function getKeyUsers(config: IConfig) {
for (const user of users) {
const keyUser = {
id: user.id,
name: user.keyName,
pubkey: user.userPubkey,
description: user.description || undefined,
createdAt: user.createdAt,
lastUsedAt: user.lastUsedAt || undefined,
revokedAt: user.revokedAt || undefined,
signingConditions: user.signingConditions, // Include signing conditions
};
@ -152,7 +154,13 @@ function callbackForKeyAdminInterface(keyName: string, adminInterface: AdminInte
return keyAllowed;
}
return adminInterface.requestPermission(keyName, remotePubkey, method, param);
const requestedPerm = await adminInterface.requestPermission(keyName, remotePubkey, method, param);
if (requestedPerm === undefined) {
throw new Error('adminInterface.requestPermission returned undefined');
}
return requestedPerm;
} catch(e) {
console.log('callbackForKey error:', e);
}
@ -205,8 +213,24 @@ class Daemon {
this.ndk = new NDK({
explicitRelayUrls: config.nostr.relays,
});
this.ndk.pool.on('connect', (r) => { console.log(`✅ Connected to ${r.url}`); });
this.ndk.pool.on('relay:connect', (r) => {
if (r) {
console.log(`✅ Connected to ${r.url}`);
} else {
console.log('✅ Connected to relays', this.ndk.pool.urls);
}
});
this.ndk.pool.on('notice', (n, r) => { console.log(`👀 Notice from ${r.url}`, n); });
this.ndk.pool.on('relay:disconnect', (r) => {
console.log(`🚫 Disconnected from ${r.url}`);
});
setInterval(() => {
const stats = this.ndk.pool.stats();
console.log(`📡 ${stats.connected} connected, ${stats.disconnected} disconnected, ${stats.connecting} connecting`);
}, 10000);
}
async start() {

19
src/utils/dm-user.ts Normal file
View file

@ -0,0 +1,19 @@
import NDK, { NDKUser, NDKEvent, NostrEvent } from "@nostr-dev-kit/ndk";
export async function dmUser(ndk: NDK, recipient: NDKUser | string, content: string): Promise<NDKEvent> {
let targetUser;
if (typeof recipient === 'string') {
targetUser = new NDKUser({ npub: recipient });
} else if (recipient instanceof NDKUser) {
targetUser = recipient;
}
const event = new NDKEvent(ndk, { kind: 4, content } as NostrEvent);
event.tag(targetUser);
await event.encrypt(targetUser);
await event.sign();
await event.publish();
return event;
}