mirror of
https://github.com/kind-0/nsecbunkerd.git
synced 2026-08-01 07:21:42 +00:00
BREAKING CHANGE: Admin API now uses kind 24133 instead of 24134 This release adds 11 new admin methods and ensures full NIP-46 compliance by switching from the non-standard kind 24134 to the spec-compliant 24133. ## New Admin Methods ### Key Management - `get_key` - Get details about a specific key - `delete_key` - Soft-delete a key and its tokens - `rotate_key` - Create new key and migrate permissions ### Policy Management - `get_policy` - Get policy details with rules - `delete_policy` - Soft-delete a policy ### Permission Management - `grant_permission` - Grant user access with a policy - `revoke_permission` - Revoke user's access to a key - `get_permissions` - Get user's permissions on a key ### Token Management - `get_token` - Get token details - `revoke_token` - Soft-delete a token - `validate_token` - Check if a token is valid ## NIP-46 Compliance Changed all admin communication from kind 24134 to kind 24133 (NDKKind.NostrConnect) to comply with the NIP-46 specification. Kind 24134 was never part of the official spec. ## Other Changes - Added Vitest test framework with 93 tests - Fixed TypeScript compilation errors - Updated documentation with breaking change notice - Version bump to 0.11.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
203 lines
6.0 KiB
TypeScript
203 lines
6.0 KiB
TypeScript
import "websocket-polyfill";
|
|
import NDK, { NDKUser, NDKEvent, NDKPrivateKeySigner, NDKNip46Signer, NostrEvent } from '@nostr-dev-kit/ndk';
|
|
import fs from 'fs';
|
|
|
|
const args = process.argv;
|
|
const command = process.argv[2];
|
|
let remotePubkey = process.argv[3];
|
|
let content = process.argv[4];
|
|
const dontPublish = process.argv.includes('--dont-publish');
|
|
const debug = process.argv.includes('--debug');
|
|
let signer: NDKNip46Signer;
|
|
let ndk: NDK;
|
|
let remoteUser: NDKUser;
|
|
|
|
const relaysIndex = args.findIndex(arg => arg === '--relays');
|
|
let relays: string[] = [];
|
|
|
|
if (relaysIndex !== -1 && args[relaysIndex + 1]) {
|
|
relays = args[relaysIndex + 1].split(',');
|
|
}
|
|
|
|
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>: sign flow: event JSON to sign (no need for pubkey or id fields) | or kind:1 content string to sign\n`);
|
|
console.log(`\t create_account flow: [desired-nip05[,desired-domain,[email]]]`);
|
|
console.log('\t--debug: enable debug mode');
|
|
console.log('\t--relays: list of relays to publish to (separated by commas)');
|
|
process.exit(1);
|
|
}
|
|
|
|
async function createNDK(): Promise<NDK> {
|
|
const ndk = new NDK({
|
|
explicitRelayUrls: [
|
|
'wss://relay.nsecbunker.com',
|
|
...relays
|
|
],
|
|
enableOutboxModel: false
|
|
});
|
|
if (debug) {
|
|
ndk.pool.on('relay: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 () => {
|
|
let remoteUser: NDKUser;
|
|
|
|
ndk = await createNDK();
|
|
|
|
// if this is the create_account command and we have something that doesn't look like an npub as the remotePubkey, use NDKUser.fromNip05 to get the npub
|
|
if (command === 'create_account' && !remotePubkey.startsWith("npub")) {
|
|
// see if we have a username@domain
|
|
let [ username, domain ] = remotePubkey.split('@');
|
|
|
|
if (!domain) {
|
|
domain = username;
|
|
username = Math.random().toString(36).substring(2, 15);
|
|
}
|
|
|
|
content = `${username},${domain}`
|
|
|
|
const u = await NDKUser.fromNip05(domain, ndk);
|
|
if (!u) {
|
|
console.log(`Invalid nip05 ${remotePubkey}`);
|
|
process.exit(1);
|
|
}
|
|
remoteUser = u;
|
|
remotePubkey = remoteUser.pubkey;
|
|
} else {
|
|
// check if we have a @ so we try to get the npub from nip05
|
|
if (remotePubkey.includes('@')) {
|
|
const u = await NDKUser.fromNip05(remotePubkey, ndk);
|
|
if (!u) {
|
|
console.log(`Invalid nip05 ${remotePubkey}`);
|
|
process.exit(1);
|
|
}
|
|
remoteUser = u;
|
|
remotePubkey = remoteUser.pubkey;
|
|
} else {
|
|
remoteUser = new NDKUser({npub: remotePubkey});
|
|
}
|
|
}
|
|
|
|
|
|
let localSigner: NDKPrivateKeySigner;
|
|
|
|
const pk = loadPrivateKey();
|
|
|
|
if (pk) {
|
|
localSigner = new NDKPrivateKeySigner(pk);
|
|
} else {
|
|
localSigner = NDKPrivateKeySigner.generate();
|
|
savePrivateKey(localSigner.privateKey!);
|
|
}
|
|
|
|
signer = new NDKNip46Signer(ndk, remoteUser.pubkey, localSigner);
|
|
if (debug) console.log(`local pubkey`, (await localSigner.user()).npub);
|
|
if (debug) console.log(`remote pubkey`, remotePubkey);
|
|
ndk.signer = signer;
|
|
|
|
signer.on("authUrl", (url) => {
|
|
console.log(`Go to ${url} to authorize this request`);
|
|
});
|
|
|
|
switch (command) {
|
|
case "sign": return signFlow();
|
|
case "create_account": return createAccountFlow();
|
|
default:
|
|
console.log(`Unknown command ${command}`);
|
|
process.exit(1);
|
|
}
|
|
})();
|
|
|
|
async function createAccountFlow() {
|
|
const [ username, domain, email ] = content.split(',').map((s) => s.trim());
|
|
try {
|
|
const pubkey = await signer.createAccount(username, domain, email);
|
|
const user = new NDKUser({pubkey});
|
|
console.log(`Hello`, user.npub);
|
|
} catch (e) {
|
|
console.log('error', e);
|
|
}
|
|
}
|
|
|
|
function signFlow() {
|
|
setTimeout(async () => {
|
|
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);
|
|
|
|
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);
|
|
}
|
|
|
|
try {
|
|
await event.sign();
|
|
if (debug) {
|
|
console.log(event.rawEvent());
|
|
} else {
|
|
console.log(event.sig);
|
|
}
|
|
|
|
if (!dontPublish) {
|
|
const relaysPublished = await event.publish();
|
|
}
|
|
|
|
process.exit(0);
|
|
} catch(e) {
|
|
console.log('sign error', e);
|
|
}
|
|
}, 2000);
|
|
}
|