From 6f16cafd4193f50db865a23ea4245eabf1a89233 Mon Sep 17 00:00:00 2001
From: pablof7z
Date: Thu, 6 Jul 2023 22:29:31 +0200
Subject: [PATCH] =?UTF-8?q?lots=20of=20things=20that=20I=20forgot=20to=20c?=
=?UTF-8?q?ommit=20=F0=9F=98=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 4 +
package-lock.json | 122 ++++++++-----
package.json | 10 +-
.../20230703092203_revokedat/migration.sql | 2 +
prisma/schema.prisma | 1 +
src/client.ts | 127 +++++++++++---
src/commands/start.ts | 2 +-
src/daemon/admin/commands/create_new_key.ts | 1 -
src/daemon/admin/commands/revoke_user.ts | 24 +++
src/daemon/admin/index.ts | 163 ++++++++++++------
.../admin/validations/request-from-admin.ts | 18 ++
src/daemon/run.ts | 28 ++-
src/utils/dm-user.ts | 19 ++
13 files changed, 393 insertions(+), 128 deletions(-)
create mode 100644 prisma/migrations/20230703092203_revokedat/migration.sql
create mode 100644 src/daemon/admin/commands/revoke_user.ts
create mode 100644 src/daemon/admin/validations/request-from-admin.ts
create mode 100644 src/utils/dm-user.ts
diff --git a/README.md b/README.md
index 3e7094e..6e9ff9b 100644
--- a/README.md
+++ b/README.md
@@ -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 "hi, I'm signing from the command line with my nsecbunkerd!"
+```
# Authors
diff --git a/package-lock.json b/package-lock.json
index 83ffefe..b563c4a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 5ff5020..00e3664 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
diff --git a/prisma/migrations/20230703092203_revokedat/migration.sql b/prisma/migrations/20230703092203_revokedat/migration.sql
new file mode 100644
index 0000000..d25002a
--- /dev/null
+++ b/prisma/migrations/20230703092203_revokedat/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "KeyUser" ADD COLUMN "revokedAt" DATETIME;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 817166b..91e0564 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -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[]
diff --git a/src/client.ts b/src/client.ts
index e38144f..714caa4 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -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 ');
+if (!command) {
+ console.log('Usage: node src/client.js [--dont-publish] [--debug] [--pk ]');
console.log('');
+ console.log(`\t: command to run (ping, sign)`);
console.log(`\t: npub that should be published as`);
- console.log(`\t: event JSON to sign | or kind:1 content string to sign`);
+ console.log(`\t: 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 {
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 () => {
- await signer.blockUntilReady();
- console.log(`authorized to sign as`, remotePubkey);
+ try {
+ if (debug) console.log(`waiting for authorization (check your nsecBunker)...`);
+ await signer.blockUntilReady();
+ } 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(),
- kind: 1,
- content,
- tags: [
- ['client', 'nsecbunker-client']
- ],
- } as NostrEvent);
+ let event;
- await event.sign();
- console.log('resulting event', JSON.stringify(await event.toNostrEvent()));
- await event.publish();
+ 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();
+ 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);
})();
\ No newline at end of file
diff --git a/src/commands/start.ts b/src/commands/start.ts
index 92e1945..5a0e2c0 100644
--- a/src/commands/start.ts
+++ b/src/commands/start.ts
@@ -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;
}
diff --git a/src/daemon/admin/commands/create_new_key.ts b/src/daemon/admin/commands/create_new_key.ts
index a7d74e7..640034d 100644
--- a/src/daemon/admin/commands/create_new_key.ts
+++ b/src/daemon/admin/commands/create_new_key.ts
@@ -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);
diff --git a/src/daemon/admin/commands/revoke_user.ts b/src/daemon/admin/commands/revoke_user.ts
new file mode 100644
index 0000000..409015e
--- /dev/null
+++ b/src/daemon/admin/commands/revoke_user.ts
@@ -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);
+}
diff --git a/src/daemon/admin/index.ts b/src/daemon/admin/index.ts
index eaa7440..892d0c1 100644
--- a/src/daemon/admin/index.ts
+++ b/src/daemon/admin/index.ts
@@ -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 {
+ 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 {
+ ): Promise {
const keyUser = await prisma.keyUser.findUnique({
where: {
unique_key_user: {
@@ -254,53 +281,81 @@ 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());
+ const params = JSON.stringify({
+ keyName,
+ remotePubkey,
+ method,
+ param,
+ description: keyUser?.description,
+ });
+
this.rpc.sendRequest(
remoteUser.hexpubkey(),
'acl',
- [JSON.stringify({
- keyName,
- remotePubkey,
- method,
- param,
- description: keyUser?.description,
- })],
- 24134, // 24134
+ [params],
+ 24134,
(res: NDKRpcResponse) => {
- let resObj;
- try {
- resObj = JSON.parse(res.result);
- } catch (e) {
- console.log('error parsing result', e);
- return;
- }
-
- console.log('request result', resObj);
-
- switch (resObj[0]) {
- case 'always': {
- allowAllRequestsFromKey(
- remotePubkey,
- keyName,
- method,
- param,
- resObj[1],
- resObj[2]
- ).then(() => {
- resolve(true);
- });
- break;
- }
- default:
- console.log('request result', res.result);
- }
+ 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);
+ } catch (e) {
+ console.log('error parsing result', e);
+ return;
+ }
+
+ switch (resObj[0]) {
+ case 'always': {
+ allowAllRequestsFromKey(
+ remotePubkey,
+ keyName,
+ method,
+ param,
+ resObj[1],
+ resObj[2]
+ );
+ resolve(true);
+ break;
+ }
+ case 'never': {
+ console.log('not implemented');
+ break;
+ }
+ default:
+ console.log('request result', res.result);
+ }
+ }
}
-export default AdminInterface;
\ No newline at end of file
+export default AdminInterface;
diff --git a/src/daemon/admin/validations/request-from-admin.ts b/src/daemon/admin/validations/request-from-admin.ts
new file mode 100644
index 0000000..38ccd04
--- /dev/null
+++ b/src/daemon/admin/validations/request-from-admin.ts
@@ -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 {
+ 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);
+}
\ No newline at end of file
diff --git a/src/daemon/run.ts b/src/daemon/run.ts
index d58be66..9b14f7d 100644
--- a/src/daemon/run.ts
+++ b/src/daemon/run.ts
@@ -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() {
diff --git a/src/utils/dm-user.ts b/src/utils/dm-user.ts
new file mode 100644
index 0000000..3ed1fe3
--- /dev/null
+++ b/src/utils/dm-user.ts
@@ -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 {
+ 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;
+}