updates
This commit is contained in:
parent
54de9cfa8e
commit
74cd9715ac
12 changed files with 3353 additions and 438 deletions
75
README.md
75
README.md
|
|
@ -1,15 +1,78 @@
|
||||||
# nsecbunker
|
# nsecbunkerd
|
||||||
Daemon to remotely sign nostr events using keys.
|
Daemon to remotely sign nostr events using keys.
|
||||||
|
|
||||||
## Installation
|
## Easy setup via docker
|
||||||
|
|
||||||
|
To quickly install `nsecbunkerd` via Docker just run:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm i -g nsecbunker
|
docker run -d --name nsecbunkerd pablof7z/nsecbunkerd start --admin <your-npub>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
nsecBunker will give you a connection string like:
|
||||||
|
|
||||||
|
```
|
||||||
|
bunker://npub1tj2dmc4udvgafxxxxxxxrtgne8j8l6rgrnaykzc8sys9mzfcz@relay.nsecbunker.com
|
||||||
|
```
|
||||||
|
|
||||||
|
You can visit https://app.nsecbunker.com/ to administrate your nsecBunker remotely.
|
||||||
|
|
||||||
|
## Hard setup:
|
||||||
|
(If you installed via docker you don't need to do any of this, skip to the [Configure](#configure) section)
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone <nsecbunkerd-repo>
|
||||||
|
npm i
|
||||||
|
npm run build
|
||||||
|
npx prisma migrate deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configure
|
||||||
|
|
||||||
|
### Easy: Remote configuration
|
||||||
|
|
||||||
|
Using the connection string you saw before, you can go to https://app.nsecbunker.com and paste your connection string.
|
||||||
|
|
||||||
|
Note that ONLY the npub that you designated as an administrator when launching nsecBunker is able to control your nsecBunker. Even if someone sees your connection string, without access to your administrator keys, there's nothing they can do.
|
||||||
|
|
||||||
|
### Hard: manual configuration
|
||||||
|
|
||||||
|
(If you are using remote configuration you don't need to do any of this)
|
||||||
|
|
||||||
|
### Add your nsec to nsecBunker
|
||||||
|
|
||||||
|
Here you'll give nsecBunker your nsec. It will ask you for a passphrase to encrypt it on-disk.
|
||||||
|
The name is an internal name you'll use to refer to this keypair. Choose anything that is useful to you.
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run nsecbunkerd -- add --name <your-key-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
```
|
||||||
|
$ npm run nsecbunkerd -- add --name "Uncomfortable family"
|
||||||
|
|
||||||
|
nsecBunker uses a passphrase to encrypt your nsec when stored on-disk.
|
||||||
|
Every time you restart it, you will need to type in this password.
|
||||||
|
|
||||||
|
Enter a passphrase: <enter-your-passphrase-here>
|
||||||
|
Enter the nsec for Uncomfortable family: <copy-your-nsec-here>
|
||||||
|
nsecBunker generated an admin password for you:
|
||||||
|
|
||||||
|
***************************
|
||||||
|
|
||||||
|
You will need this to manage users of your keys.
|
||||||
|
````
|
||||||
|
|
||||||
|
## Start
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm run nsecbunkerd start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with `nsecbunker-client`
|
||||||
|
|
||||||
|
|
||||||
nsecbunker -
|
|
||||||
|
|
||||||
# Authors
|
# Authors
|
||||||
|
|
||||||
|
|
@ -18,5 +81,5 @@ nsecbunker -
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
CC BY-NC-ND 3.0
|
CC BY-NC-ND 4.0
|
||||||
Contact @pablof7z for licensing.
|
Contact @pablof7z for licensing.
|
||||||
2977
package-lock.json
generated
2977
package-lock.json
generated
File diff suppressed because it is too large
Load diff
25
package.json
25
package.json
|
|
@ -1,14 +1,18 @@
|
||||||
{
|
{
|
||||||
"name": "nsecbunker",
|
"name": "nsecbunkerd",
|
||||||
"version": "0.1.0",
|
"version": "0.5.8",
|
||||||
"description": "nsecbunker",
|
"description": "nsecbunker daemon",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"nsecbunkerd": "dist/index.js",
|
"nsecbunkerd": "dist/index.js",
|
||||||
"nsecbunker-client": "dist/client.js"
|
"nsecbunker-client": "dist/client.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist",
|
||||||
|
"scripts/start.js",
|
||||||
|
"prisma/schema.prisma",
|
||||||
|
"LICENSE",
|
||||||
|
"README.md"
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -16,7 +20,10 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/index.js",
|
"prisma:generate": "npx prisma generate",
|
||||||
|
"prisma:migrate": "npx prisma migrate deploy",
|
||||||
|
"prisma:create": "npx prisma db push --preview-feature",
|
||||||
|
"start": "node ./scripts/start.js",
|
||||||
"nsecbunkerd": "node dist/index.js",
|
"nsecbunkerd": "node dist/index.js",
|
||||||
"nsecbunker-client": "node dist/client.js"
|
"nsecbunker-client": "node dist/client.js"
|
||||||
},
|
},
|
||||||
|
|
@ -24,16 +31,16 @@
|
||||||
"nostr"
|
"nostr"
|
||||||
],
|
],
|
||||||
"author": "pablof7z",
|
"author": "pablof7z",
|
||||||
"license": "MIT",
|
"license": "CC BY-NC-ND 4.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/password": "^1.0.0",
|
"@inquirer/password": "^1.0.0",
|
||||||
"@inquirer/prompts": "^1.0.0",
|
"@inquirer/prompts": "^1.0.0",
|
||||||
"@prisma/client": "4.13.0",
|
"@nostr-dev-kit/ndk": "^0.3.26",
|
||||||
|
"@prisma/client": "^4.14.1",
|
||||||
"@scure/base": "^1.1.1",
|
"@scure/base": "^1.1.1",
|
||||||
"@types/yargs": "^17.0.24",
|
"@types/yargs": "^17.0.24",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
||||||
"@typescript-eslint/parser": "^5.57.0",
|
"@typescript-eslint/parser": "^5.57.0",
|
||||||
"crypto": "^1.0.1",
|
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"eslint": "^8.37.0",
|
"eslint": "^8.37.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
|
@ -45,7 +52,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
"prisma": "^4.13.0",
|
"prisma": "^4.14.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.0.3"
|
"typescript": "^5.0.3"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,19 @@ generator client {
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
url = env("DATABASE_URL")
|
url = "file:./nsecbunker.db"
|
||||||
}
|
}
|
||||||
|
|
||||||
model KeyUser {
|
model KeyUser {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
keyName String
|
keyName String
|
||||||
userPubkey String
|
userPubkey String
|
||||||
|
description String?
|
||||||
signingConditions SigningCondition[]
|
signingConditions SigningCondition[]
|
||||||
logs Log[]
|
logs Log[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
lastUsedAt DateTime?
|
||||||
|
|
||||||
@@unique([keyName, userPubkey], name: "unique_key_user")
|
@@unique([keyName, userPubkey], name: "unique_key_user")
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +28,7 @@ model SigningCondition {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
|
||||||
method String?
|
method String?
|
||||||
kind Int?
|
kind String?
|
||||||
content String?
|
content String?
|
||||||
keyUserKeyName String?
|
keyUserKeyName String?
|
||||||
allowed Boolean?
|
allowed Boolean?
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,33 @@
|
||||||
import NDK, { NDKEvent, NDKPrivateKeySigner, NDKNip46Signer, NostrEvent } from '@nostr-dev-kit/ndk';
|
import NDK, { NDKUser, NDKEvent, NDKPrivateKeySigner, NDKNip46Signer, NostrEvent } from '@nostr-dev-kit/ndk';
|
||||||
|
|
||||||
const remotePubkey = process.env.PUBKEY;
|
const remotePubkey = process.argv[2];
|
||||||
|
|
||||||
if (!remotePubkey) {
|
|
||||||
console.log('Usage: PUBKEY=<pubkey> node src/client.js <content>');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pubkey = process.argv[2];
|
|
||||||
const content = process.argv[3];
|
const content = process.argv[3];
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
console.log('Usage: node src/client.js <remote-pubkey> <content>');
|
console.log('Usage: node src/client.js <remote-npub> <content>');
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(`\t<remote-pubkey>: npub that should be published as`);
|
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 | or kind:1 content string to sign`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNDK(): Promise<NDK> {
|
async function createNDK(): Promise<NDK> {
|
||||||
const ndk = new NDK({
|
const ndk = new NDK({
|
||||||
explicitRelayUrls: ['wss://nos.lol'],
|
explicitRelayUrls: ['wss://nostr.vulpem.com', "wss://67aee52897df.ngrok.app"],
|
||||||
});
|
});
|
||||||
await ndk.connect(2000);
|
ndk.pool.on('relay:connect', () => console.log('✅ connected'));
|
||||||
ndk.pool.on('connect', () => console.log('✅ connected'));
|
ndk.pool.on('relay:disconnect', () => console.log('❌ disconnected'));
|
||||||
|
await ndk.connect(5000);
|
||||||
|
|
||||||
return ndk;
|
return ndk;
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
const remoteUser = new NDKUser({npub: remotePubkey});
|
||||||
const ndk = await createNDK();
|
const ndk = await createNDK();
|
||||||
const localSigner = new NDKPrivateKeySigner('9ec8a4b2e1fac9eae616736f718f92ed30c57fc2fff36ef8139e27c31889e327');
|
const localSigner = NDKPrivateKeySigner.generate();
|
||||||
const signer = new NDKNip46Signer(ndk, remotePubkey, localSigner);
|
// const localSigner = new NDKPrivateKeySigner('b8baad35c387d7cf84d52e0958d9a02aff214393a85b0703de4146c7a3697bb3');
|
||||||
|
const signer = new NDKNip46Signer(ndk, remoteUser.hexpubkey(), localSigner);
|
||||||
console.log(`local pubkey`, (await signer.user()).npub);
|
console.log(`local pubkey`, (await signer.user()).npub);
|
||||||
console.log(`remote pubkey`, remotePubkey);
|
console.log(`remote pubkey`, remotePubkey);
|
||||||
ndk.signer = signer;
|
ndk.signer = signer;
|
||||||
|
|
@ -40,17 +36,17 @@ async function createNDK(): Promise<NDK> {
|
||||||
await signer.blockUntilReady();
|
await signer.blockUntilReady();
|
||||||
console.log(`authorized to sign as`, remotePubkey);
|
console.log(`authorized to sign as`, remotePubkey);
|
||||||
|
|
||||||
const notPabloEvent = new NDKEvent(ndk, {
|
const event = new NDKEvent(ndk, {
|
||||||
pubkey: remotePubkey,
|
pubkey: remoteUser.hexpubkey(),
|
||||||
kind: 1,
|
kind: 1,
|
||||||
content,
|
content,
|
||||||
tags: [
|
tags: [
|
||||||
['t', 'grownostr'],
|
['client', 'nsecbunker-client']
|
||||||
],
|
],
|
||||||
} as NostrEvent);
|
} as NostrEvent);
|
||||||
|
|
||||||
await notPabloEvent.sign();
|
await event.sign();
|
||||||
console.log('resulting event', JSON.stringify(await notPabloEvent.toNostrEvent()));
|
console.log('resulting event', JSON.stringify(await event.toNostrEvent()));
|
||||||
// await notPabloEvent.publish();
|
await event.publish();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
})();
|
})();
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {nip19, getPublicKey} from 'nostr-tools';
|
import {nip19} from 'nostr-tools';
|
||||||
import readline from 'readline';
|
import readline from 'readline';
|
||||||
import { getCurrentConfig, saveCurrentConfig } from '../config/index.js';
|
import { getCurrentConfig, saveCurrentConfig } from '../config/index.js';
|
||||||
import { encryptNsec } from '../config/keys.js';
|
import { encryptNsec } from '../config/keys.js';
|
||||||
|
|
@ -8,9 +8,9 @@ interface IOpts {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveEncrypted(config: string, nsec: string, passphrase: string, name: string) {
|
export async function saveEncrypted(config: string, nsec: string, passphrase: string, name: string) {
|
||||||
const { iv, data } = encryptNsec(nsec, passphrase);
|
const { iv, data } = encryptNsec(nsec, passphrase);
|
||||||
const currentConfig = getCurrentConfig(config);
|
const currentConfig = await getCurrentConfig(config);
|
||||||
|
|
||||||
currentConfig.keys[name] = { iv, data };
|
currentConfig.keys[name] = { iv, data };
|
||||||
|
|
||||||
|
|
@ -35,8 +35,8 @@ export async function addNsec(opts: IOpts) {
|
||||||
let decoded;
|
let decoded;
|
||||||
try {
|
try {
|
||||||
decoded = nip19.decode(nsec);
|
decoded = nip19.decode(nsec);
|
||||||
const hexpubkey = getPublicKey(decoded.data as string);
|
// const hexpubkey = getPublicKey(decoded.data as string);
|
||||||
const npub = nip19.npubEncode(hexpubkey);
|
// const npub = nip19.npubEncode(hexpubkey);
|
||||||
saveEncrypted(config, nsec, passphrase, name);
|
saveEncrypted(config, nsec, passphrase, name);
|
||||||
|
|
||||||
rl.close();
|
rl.close();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,21 @@
|
||||||
import readline from 'readline';
|
import readline from 'readline';
|
||||||
import fs from 'fs';
|
import { getCurrentConfig, saveCurrentConfig } from '../config/index.js';
|
||||||
import crypto from 'crypto';
|
|
||||||
|
|
||||||
export function setup(config: string) {
|
export async function setup(config: string) {
|
||||||
|
const currentConfig = await getCurrentConfig(config);
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`You need at least one administrator to remotely control nsecBunker. This should probably be your own npub.\n`);
|
||||||
|
|
||||||
|
rl.question(`Enter an administrator npub: `, (npub: string) => {
|
||||||
|
currentConfig.admin.npubs.push(npub);
|
||||||
|
|
||||||
|
saveCurrentConfig(config, currentConfig);
|
||||||
|
rl.close();
|
||||||
|
|
||||||
|
console.log(`Administrator npub added!`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import readline from 'readline';
|
import readline from 'readline';
|
||||||
import { getCurrentConfig } from '../config/index.js';
|
import { getCurrentConfig, saveCurrentConfig } from '../config/index.js';
|
||||||
import { decryptNsec } from '../config/keys.js';
|
import { decryptNsec } from '../config/keys.js';
|
||||||
import { fork } from 'child_process';
|
import { fork } from 'child_process';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
|
@ -8,10 +8,21 @@ interface IOpts {
|
||||||
keys: string[];
|
keys: string[];
|
||||||
verbose: boolean;
|
verbose: boolean;
|
||||||
config: string;
|
config: string;
|
||||||
|
adminNpubs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This command starts the nsecbunkerd process with an (optional)
|
||||||
|
* admin interface over websockets or redis.
|
||||||
|
*/
|
||||||
export async function start(opts: IOpts) {
|
export async function start(opts: IOpts) {
|
||||||
const configData = getCurrentConfig(opts.config);
|
const configData = await getCurrentConfig(opts.config);
|
||||||
|
|
||||||
|
if (opts.adminNpubs) {
|
||||||
|
configData.admin.npubs = opts.adminNpubs;
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveCurrentConfig(opts.config, configData);
|
||||||
|
|
||||||
if (opts.verbose) {
|
if (opts.verbose) {
|
||||||
configData.verbose = opts.verbose;
|
configData.verbose = opts.verbose;
|
||||||
|
|
@ -19,11 +30,7 @@ export async function start(opts: IOpts) {
|
||||||
|
|
||||||
const keys: Record<string, string> = {};
|
const keys: Record<string, string> = {};
|
||||||
|
|
||||||
let keysToStart = opts.keys;
|
const keysToStart = opts.keys || [];
|
||||||
|
|
||||||
if (!keysToStart) {
|
|
||||||
keysToStart = Object.keys(configData.keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const keyName of keysToStart) {
|
for (const keyName of keysToStart) {
|
||||||
const nsec = await startKey(keyName, configData.keys[keyName], opts.verbose);
|
const nsec = await startKey(keyName, configData.keys[keyName], opts.verbose);
|
||||||
|
|
@ -33,19 +40,13 @@ export async function start(opts: IOpts) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(keys).length === 0) {
|
|
||||||
console.log(`No keys started.`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`nsecBunker starting with keys:`, Object.keys(keys).join(', '));
|
|
||||||
|
|
||||||
configData.keys = keys;
|
|
||||||
|
|
||||||
const daemonProcess = fork(resolve(__dirname, '../daemon/index.js'));
|
const daemonProcess = fork(resolve(__dirname, '../daemon/index.js'));
|
||||||
daemonProcess.send(configData);
|
daemonProcess.send({
|
||||||
|
configFile: opts.config,
|
||||||
// process.exit(0);
|
allKeys: configData.keys,
|
||||||
|
...configData,
|
||||||
|
keys,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KeyData {
|
interface KeyData {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,34 @@
|
||||||
import { randomBytes } from 'crypto';
|
|
||||||
import { readFileSync, writeFileSync } from 'fs';
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||||
|
import { IAdminOpts } from '../daemon/admin';
|
||||||
|
|
||||||
function getPassphrase(): string {
|
const generatedKey = NDKPrivateKeySigner.generate();
|
||||||
const passwordLength = 32;
|
|
||||||
const passwordBytes = randomBytes(passwordLength);
|
export interface IConfig {
|
||||||
return passwordBytes.toString('base64').slice(0, passwordLength);
|
nostr: {
|
||||||
|
relays: string[];
|
||||||
|
};
|
||||||
|
admin: IAdminOpts;
|
||||||
|
database: string;
|
||||||
|
logs: string;
|
||||||
|
keys: Record<string, any>;
|
||||||
|
verbose: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig: IConfig = {
|
||||||
nostr: {
|
nostr: {
|
||||||
relays: [
|
relays: [
|
||||||
'wss://nos.lol',
|
'wss://nos.lol',
|
||||||
// 'wss://relay.damus.io'
|
// 'wss://relay.damus.io'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
remote: {
|
admin: {
|
||||||
passphrase: getPassphrase(),
|
npubs: [],
|
||||||
|
adminRelays: [
|
||||||
|
"wss://nostr.vulpem.com",
|
||||||
|
"wss://relay.nsecbunker.com"
|
||||||
|
],
|
||||||
|
key: generatedKey.privateKey!
|
||||||
},
|
},
|
||||||
database: 'sqlite://nsecbunker.db',
|
database: 'sqlite://nsecbunker.db',
|
||||||
logs: './nsecbunker.log',
|
logs: './nsecbunker.log',
|
||||||
|
|
@ -23,17 +36,13 @@ const defaultConfig = {
|
||||||
verbose: false,
|
verbose: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getCurrentConfig(config: string) {
|
async function getCurrentConfig(config: string): Promise<IConfig> {
|
||||||
try {
|
try {
|
||||||
const configFileContents = readFileSync(config, 'utf8');
|
const configFileContents = readFileSync(config, 'utf8');
|
||||||
return JSON.parse(configFileContents);
|
return JSON.parse(configFileContents);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
const d = defaultConfig;
|
await saveCurrentConfig(config, defaultConfig);
|
||||||
|
|
||||||
console.log(`nsecBunker generated an admin password for you:\n\n${d.remote.passphrase}\n\n` +
|
|
||||||
`You will need this to manage users of your keys.\n\n`);
|
|
||||||
|
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error reading config file: ${err.message}`);
|
console.error(`Error reading config file: ${err.message}`);
|
||||||
|
|
@ -51,3 +60,5 @@ export function saveCurrentConfig(config: string, currentConfig: any) {
|
||||||
process.exit(1); // Kill the process if there is an error parsing the JSON
|
process.exit(1); // Kill the process if there is an error parsing the JSON
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export {getCurrentConfig};
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import run from './run';
|
import run from './run';
|
||||||
import type {IOpts} from './run';
|
import type {IConfig} from '../config/index';
|
||||||
|
|
||||||
process.on('message', (configData: IOpts) => {
|
export type DaemonConfig = IConfig & {
|
||||||
run(configData);
|
configFile: string;
|
||||||
|
allKeys: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('message', (config: DaemonConfig) => {
|
||||||
|
run(config);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,222 +1,104 @@
|
||||||
import NDK, { Nip46PermitCallback } from '@nostr-dev-kit/ndk';
|
import NDK, { NDKPrivateKeySigner, Nip46PermitCallback } from '@nostr-dev-kit/ndk';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { Backend } from './backend/index.js';
|
import { Backend } from './backend/index.js';
|
||||||
import readline from 'readline';
|
import {
|
||||||
|
checkIfPubkeyAllowed,
|
||||||
|
allowAllRequestsFromKey,
|
||||||
|
rejectAllRequestsFromKey
|
||||||
|
} from './lib/acl/index.js';
|
||||||
|
import AdminInterface from './admin/index.js';
|
||||||
|
import { askYNquestion } from '../utils/prompts/boolean.js';
|
||||||
|
import { IConfig } from '../config/index.js';
|
||||||
|
import { NDKRpcRequest } from '@nostr-dev-kit/ndk';
|
||||||
import prisma from '../db.js';
|
import prisma from '../db.js';
|
||||||
|
import { DaemonConfig } from './index.js';
|
||||||
|
import { decryptNsec } from '../config/keys.js';
|
||||||
|
|
||||||
export interface IOpts {
|
export type Key = {
|
||||||
keys: Record<string, string>;
|
name: string;
|
||||||
nostr: {
|
npub?: string;
|
||||||
relays: string[],
|
};
|
||||||
}
|
|
||||||
verbose: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export type KeyUser = {
|
||||||
|
name: string;
|
||||||
|
pubkey: string;
|
||||||
|
description?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
lastUsedAt?: Date;
|
||||||
|
};
|
||||||
|
|
||||||
export default async function run(opts: IOpts) {
|
function getKeys(config: DaemonConfig) {
|
||||||
console.log(`nsecBunker daemon starting with PID ${process.pid}...`);
|
return async (): Promise<Key[]> => {
|
||||||
console.log(`Connecting to ${opts.nostr.relays.length} relays...`);
|
let lockedKeyNames = Object.keys(config.allKeys);
|
||||||
|
const keys: Key[] = [];
|
||||||
|
|
||||||
const ndk = new NDK({
|
for (const [name, nsec] of Object.entries(config.keys)) {
|
||||||
explicitRelayUrls: opts.nostr.relays,
|
|
||||||
});
|
|
||||||
await ndk.pool.on('connect', (r) => { console.log(`✅ Connected to ${r.url}`); });
|
|
||||||
await ndk.pool.on('notice', (n, r) => { console.log(`👀 Notice from ${r.url}`, n); });
|
|
||||||
await ndk.connect(5000);
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
const promise = [];
|
|
||||||
|
|
||||||
for (const [name, nsec] of Object.entries(opts.keys)) {
|
|
||||||
const cb = callbackForKey(name);
|
|
||||||
const hexpk = nip19.decode(nsec).data as string;
|
const hexpk = nip19.decode(nsec).data as string;
|
||||||
const backend = new Backend(ndk, hexpk, cb);
|
const user = await new NDKPrivateKeySigner(hexpk).user();
|
||||||
promise.push(backend.start());
|
const key = {
|
||||||
|
name,
|
||||||
|
npub: user.npub,
|
||||||
|
};
|
||||||
|
|
||||||
|
lockedKeyNames = lockedKeyNames.filter((keyName) => keyName !== name);
|
||||||
|
keys.push(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promise);
|
console.log({ lockedKeyNames });
|
||||||
|
|
||||||
console.log('✅ nsecBunker ready to serve requests.');
|
for (const name of lockedKeyNames) {
|
||||||
}, 1000);
|
keys.push({ name });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkIfPubkeyAllowed(keyName: string, remotePubkey: string, method: string, param?: any): Promise<boolean | undefined> {
|
return keys;
|
||||||
// find KeyUser
|
};
|
||||||
const keyUser = await prisma.keyUser.findUnique({
|
|
||||||
where: { unique_key_user: { keyName, userPubkey: remotePubkey } },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!keyUser) {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// find SigningCondition
|
function getKeyUsers(config: IConfig) {
|
||||||
const signingConditionQuery = requestToSigningConditionQuery(method, param);
|
return async (req: NDKRpcRequest): Promise<KeyUser[]> => {
|
||||||
|
const keyUsers: KeyUser[] = [];
|
||||||
|
const keyName = req.params[0];
|
||||||
|
|
||||||
const explicitReject = await prisma.signingCondition.findFirst({
|
const users = await prisma.keyUser.findMany({
|
||||||
where: {
|
where: {
|
||||||
keyUserId: keyUser.id,
|
keyName,
|
||||||
method: '*',
|
},
|
||||||
allowed: false,
|
include: {
|
||||||
}
|
signingConditions: true,
|
||||||
});
|
|
||||||
|
|
||||||
if (explicitReject) {
|
|
||||||
console.log(`explicit reject`, explicitReject);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signingCondition = await prisma.signingCondition.findFirst({
|
|
||||||
where: {
|
|
||||||
keyUserId: keyUser.id,
|
|
||||||
...signingConditionQuery,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// if no SigningCondition found, return undefined
|
|
||||||
if (!signingCondition) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowed = signingCondition.allowed;
|
|
||||||
|
|
||||||
if (allowed === true || allowed === false) {
|
|
||||||
console.log(`found signing condition`, signingCondition);
|
|
||||||
return allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestToSigningConditionQuery(method: string, param?: any) {
|
|
||||||
const signingConditionQuery: any = { method };
|
|
||||||
|
|
||||||
switch (method) {
|
|
||||||
case 'sign_event':
|
|
||||||
signingConditionQuery.kind = param.kind;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return signingConditionQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function allowAllRequestsFromKey(remotePubkey: string, keyName: string, method: string, param?: any): Promise<void> {
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Upsert the KeyUser with the given remotePubkey
|
|
||||||
const upsertedUser = await prisma.keyUser.upsert({
|
|
||||||
where: { unique_key_user: { keyName, userPubkey: remotePubkey } },
|
|
||||||
update: { },
|
|
||||||
create: { keyName, userPubkey: remotePubkey },
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log({ upsertedUser });
|
|
||||||
|
|
||||||
// Create a new SigningCondition for the given KeyUser and set allowed to true
|
|
||||||
const signingConditionQuery = requestToSigningConditionQuery(method, param);
|
|
||||||
await prisma.signingCondition.create({
|
|
||||||
data: {
|
|
||||||
allowed: true,
|
|
||||||
keyUserId: upsertedUser.id,
|
|
||||||
...signingConditionQuery
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
console.log('allowAllRequestsFromKey', e);
|
for (const user of users) {
|
||||||
}
|
const keyUser = {
|
||||||
|
name: user.keyName,
|
||||||
|
pubkey: user.userPubkey,
|
||||||
|
description: user.description || undefined,
|
||||||
|
createdAt: user.createdAt,
|
||||||
|
lastUsedAt: user.lastUsedAt || undefined,
|
||||||
|
signingConditions: user.signingConditions, // Include signing conditions
|
||||||
|
};
|
||||||
|
|
||||||
|
keyUsers.push(keyUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rejectAllRequestsFromKey(remotePubkey: string, keyName: string): Promise<void> {
|
return keyUsers;
|
||||||
// Upsert the KeyUser with the given remotePubkey
|
};
|
||||||
const upsertedUser = await prisma.keyUser.upsert({
|
|
||||||
where: { unique_key_user: { keyName, userPubkey: remotePubkey } },
|
|
||||||
update: { },
|
|
||||||
create: { keyName, userPubkey: remotePubkey },
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log({ upsertedUser });
|
|
||||||
|
|
||||||
// Create a new SigningCondition for the given KeyUser and set allowed to false
|
|
||||||
await prisma.signingCondition.create({
|
|
||||||
data: {
|
|
||||||
allowed: false,
|
|
||||||
keyUserId: upsertedUser.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let requestPermissionMutex = false;
|
||||||
interface IAskYNquestionOpts {
|
|
||||||
timeoutLength?: number;
|
|
||||||
yes: any;
|
|
||||||
no: any;
|
|
||||||
always?: any;
|
|
||||||
never?: any;
|
|
||||||
response?: any;
|
|
||||||
timeout?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function askYNquestion(
|
|
||||||
question: string,
|
|
||||||
opts: IAskYNquestionOpts
|
|
||||||
) {
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout
|
|
||||||
});
|
|
||||||
let timeout: NodeJS.Timeout | undefined;
|
|
||||||
|
|
||||||
if (opts.timeoutLength) {
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
rl.close();
|
|
||||||
opts.timeout && opts.timeout();
|
|
||||||
}, opts.timeoutLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
const prompts = ['y', 'n'];
|
|
||||||
|
|
||||||
if (opts.always) prompts.push('always');
|
|
||||||
if (opts.never) prompts.push('never');
|
|
||||||
|
|
||||||
question += ` (${prompts.join('/')})`;
|
|
||||||
|
|
||||||
rl.question(question, (answer) => {
|
|
||||||
timeout && clearTimeout(timeout);
|
|
||||||
|
|
||||||
switch (answer) {
|
|
||||||
case 'y':
|
|
||||||
case 'Y':
|
|
||||||
opts.yes();
|
|
||||||
opts.response && opts.response(answer);
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
case 'N':
|
|
||||||
opts.no();
|
|
||||||
opts.response && opts.response(answer);
|
|
||||||
break;
|
|
||||||
case 'always':
|
|
||||||
opts.yes();
|
|
||||||
opts.always();
|
|
||||||
opts.response && opts.response(answer);
|
|
||||||
break;
|
|
||||||
case 'never':
|
|
||||||
opts.no();
|
|
||||||
opts.never();
|
|
||||||
opts.response && opts.response(answer);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log('Invalid answer');
|
|
||||||
askYNquestion(question, opts);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
return rl;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestPermission(keyName: string, remotePubkey: string, method: string, param?: any): Promise<boolean> {
|
async function requestPermission(keyName: string, remotePubkey: string, method: string, param?: any): Promise<boolean> {
|
||||||
|
if (requestPermissionMutex) {
|
||||||
|
console.log(`can't process request ${method} because signer is busy`);
|
||||||
|
return false;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// requestPermission(keyName, remotePubkey, method, param);
|
||||||
|
// }, 1000);
|
||||||
|
// return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPermissionMutex = true;
|
||||||
|
|
||||||
const npub = nip19.npubEncode(remotePubkey);
|
const npub = nip19.npubEncode(remotePubkey);
|
||||||
|
|
||||||
const promise = new Promise<boolean>((resolve, reject) => {
|
const promise = new Promise<boolean>((resolve, reject) => {
|
||||||
|
|
@ -245,12 +127,40 @@ async function requestPermission(keyName: string, remotePubkey: string, method:
|
||||||
console.log('🚫 Denying this request and all future requests from this key.');
|
console.log('🚫 Denying this request and all future requests from this key.');
|
||||||
await rejectAllRequestsFromKey(remotePubkey, keyName);
|
await rejectAllRequestsFromKey(remotePubkey, keyName);
|
||||||
},
|
},
|
||||||
|
response: () => {
|
||||||
|
requestPermissionMutex = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function callbackForKeyAdminInterface(keyName: string, adminInterface: AdminInterface): Nip46PermitCallback {
|
||||||
|
return async (remotePubkey: string, method: string, param?: any): Promise<boolean> => {
|
||||||
|
console.log(`🔑 ${keyName} is being requested to ${method} by ${nip19.npubEncode(remotePubkey)}`);
|
||||||
|
|
||||||
|
if (!adminInterface.requestPermission) {
|
||||||
|
throw new Error('adminInterface.requestPermission is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keyAllowed = await checkIfPubkeyAllowed(keyName, remotePubkey, method, param);
|
||||||
|
|
||||||
|
if (keyAllowed === true || keyAllowed === false) {
|
||||||
|
console.log(`🔎 ${nip19.npubEncode(remotePubkey)} is ${keyAllowed ? 'allowed' : 'denied'} to ${method} with key ${keyName}`);
|
||||||
|
return keyAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return adminInterface.requestPermission(keyName, remotePubkey, method, param);
|
||||||
|
} catch(e) {
|
||||||
|
console.log('callbackForKey error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function callbackForKey(keyName: string): Nip46PermitCallback {
|
function callbackForKey(keyName: string): Nip46PermitCallback {
|
||||||
return async (remotePubkey: string, method: string, param?: any): Promise<boolean> => {
|
return async (remotePubkey: string, method: string, param?: any): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -270,3 +180,78 @@ function callbackForKey(keyName: string): Nip46PermitCallback {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default async function run(config: DaemonConfig) {
|
||||||
|
const daemon = new Daemon(config);
|
||||||
|
await daemon.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Daemon {
|
||||||
|
private config: DaemonConfig;
|
||||||
|
private activeKeys: Record<string, any>;
|
||||||
|
private adminInterface: AdminInterface;
|
||||||
|
private ndk: NDK;
|
||||||
|
|
||||||
|
constructor(config: DaemonConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.activeKeys = config.keys;
|
||||||
|
this.adminInterface = new AdminInterface(config.admin, config.configFile);
|
||||||
|
|
||||||
|
this.adminInterface.getKeys = getKeys(config);
|
||||||
|
this.adminInterface.getKeyUsers = getKeyUsers(config);
|
||||||
|
this.adminInterface.unlockKey = this.unlockKey.bind(this);
|
||||||
|
this.adminInterface.loadNsec = this.loadNsec.bind(this);
|
||||||
|
|
||||||
|
this.ndk = new NDK({
|
||||||
|
explicitRelayUrls: config.nostr.relays,
|
||||||
|
});
|
||||||
|
this.ndk.pool.on('connect', (r) => { console.log(`✅ Connected to ${r.url}`); });
|
||||||
|
this.ndk.pool.on('notice', (n, r) => { console.log(`👀 Notice from ${r.url}`, n); });
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
await this.ndk.connect(5000);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
const promise = [];
|
||||||
|
|
||||||
|
for (const [name, nsec] of Object.entries(this.config.keys)) {
|
||||||
|
promise.push(this.startKey(name, nsec));
|
||||||
|
}
|
||||||
|
|
||||||
|
// await Promise.all(promise);
|
||||||
|
|
||||||
|
console.log('✅ nsecBunker ready to serve requests.');
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to start a key's backend
|
||||||
|
* @param name Name of the key
|
||||||
|
* @param nsec NSec of the key
|
||||||
|
*/
|
||||||
|
async startKey(name: string, nsec: string) {
|
||||||
|
const cb = callbackForKeyAdminInterface(name, this.adminInterface);
|
||||||
|
const hexpk = nip19.decode(nsec).data as string;
|
||||||
|
const backend = new Backend(this.ndk, hexpk, cb);
|
||||||
|
await backend.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
async unlockKey(keyName: string, passphrase: string): Promise<boolean> {
|
||||||
|
const keyData = this.config.allKeys[keyName];
|
||||||
|
const { iv, data } = keyData;
|
||||||
|
|
||||||
|
const nsec = decryptNsec(iv, data, passphrase);
|
||||||
|
this.activeKeys[keyName] = nsec;
|
||||||
|
|
||||||
|
this.startKey(keyName, nsec);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNsec(keyName: string, nsec: string) {
|
||||||
|
this.activeKeys[keyName] = nsec;
|
||||||
|
|
||||||
|
this.startKey(keyName, nsec);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/index.ts
60
src/index.ts
|
|
@ -7,14 +7,14 @@ import { setup } from './commands/setup.js';
|
||||||
import { addNsec } from './commands/add.js';
|
import { addNsec } from './commands/add.js';
|
||||||
import { start } from './commands/start.js';
|
import { start } from './commands/start.js';
|
||||||
|
|
||||||
console.log(`nsecBunker licensed under CC BY-NC-ND 3.0:`);
|
console.log(`nsecBunker licensed under CC BY-NC-ND 4.0:`);
|
||||||
console.log(`free to use for non-commercial use`);
|
console.log(`free to use for non-commercial use`);
|
||||||
console.log(`Copyright by pablof7z <npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft> 2023`);
|
console.log(`Copyright by pablof7z <pablo@f7z.io> 2023`);
|
||||||
console.log(`Contact for licensing`);
|
console.log(`Contact for licensing`);
|
||||||
console.log(``);
|
console.log(``);
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.command('setup', 'Setup nsecBunker', () => {}, (argv) => {
|
.command('setup', 'Setup nsecBunker', {}, (argv) => {
|
||||||
setup(argv.config as string);
|
setup(argv.config as string);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -30,13 +30,19 @@ yargs(hideBin(process.argv))
|
||||||
.option('key <name>', {
|
.option('key <name>', {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Name of key to enable',
|
description: 'Name of key to enable',
|
||||||
|
})
|
||||||
|
.array('admin')
|
||||||
|
.option('admin <npub>', {
|
||||||
|
alias: 'a',
|
||||||
|
type: 'string',
|
||||||
|
description: 'Admin npub',
|
||||||
});
|
});
|
||||||
}, (argv) => {
|
}, (argv) => {
|
||||||
start({
|
start({
|
||||||
keys: argv.key as string[],
|
keys: argv.key as string[],
|
||||||
verbose: argv.verbose as boolean,
|
verbose: argv.verbose as boolean,
|
||||||
config: argv.config as string,
|
config: argv.config as string,
|
||||||
|
adminNpubs: argv.admin as string[],
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -65,49 +71,3 @@ yargs(hideBin(process.argv))
|
||||||
})
|
})
|
||||||
.demandCommand(1)
|
.demandCommand(1)
|
||||||
.parse();
|
.parse();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// async function cb(pubkey: string, method: string, param?: any): Promise<boolean> {
|
|
||||||
// // check if pubkey is in allowed list file
|
|
||||||
// // if not, return false
|
|
||||||
// // if yes, return true
|
|
||||||
|
|
||||||
// // read file allowed.json
|
|
||||||
// try {
|
|
||||||
// const data = fs.readFileSync('config.json', 'utf8');
|
|
||||||
// const config = JSON.parse(data);
|
|
||||||
// const allowedPubkeys = config.allowedPubkeys || {};
|
|
||||||
// console.log('allowedPubkeys', allowedPubkeys, allowedPubkeys[pubkey]);
|
|
||||||
// if (allowedPubkeys[pubkey] && allowedPubkeys[pubkey].methods[method]) {
|
|
||||||
// console.log(`✅ ${pubkey} is allowed to ${method}`);
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
// } catch(e) {
|
|
||||||
// console.log('Error:', e);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// console.log(`🚫 ${pubkey} is not allowed to ${method}`);
|
|
||||||
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// (async () => {
|
|
||||||
// const ndk = await createNDK();
|
|
||||||
|
|
||||||
// console.log(`NSECBUNKER BOOTING UP`);
|
|
||||||
|
|
||||||
// if (!process.env.PKEY) {
|
|
||||||
// console.error('PKEY not set');
|
|
||||||
// process.exit(1);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const backend = new Backend(ndk, process.env.PKEY, cb);
|
|
||||||
// await backend.start();
|
|
||||||
|
|
||||||
// const npub = backend.localUser?.npub;
|
|
||||||
// const hexpubkey = backend.localUser?.hexpubkey();
|
|
||||||
// console.log(`NPUB: ${npub}`);
|
|
||||||
// console.log(`PUBK: ${hexpubkey}`);
|
|
||||||
// })();
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue