mirror of
https://github.com/aljazceru/nsecbunkerd.git
synced 2025-12-17 14:14:26 +01:00
implement create_account in client
This commit is contained in:
36
OAUTH-LIKE-FLOW.md
Normal file
36
OAUTH-LIKE-FLOW.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# OAuth-like flow
|
||||||
|
|
||||||
|
The OAuth-like flow is a way to create new users in the bunker.
|
||||||
|
|
||||||
|
The goal of this flow is to provide a flow that is familiar for new users that are not familiar with key management and that doesn't requrie installing extensions.
|
||||||
|
|
||||||
|
The way it works is, a new user without a nostr account goes to an client that implements this flow, when they click register the following happen:
|
||||||
|
|
||||||
|
* the client should ask for the user desired NIP-05.
|
||||||
|
* to accomplish this, the client can hardcode using their own backend or they can use NIP-89 to find nsecbunker providers.
|
||||||
|
* if using non-trusted (i.e. from NIP-89) the client should validate that the bunker's pubkey `kind:0` has a valid NIP-05 with the `_@domain` identifier.
|
||||||
|
* the client generates a local key and stores it in the user's device. **This is the local key the client will use to sign on behalf of the user**
|
||||||
|
* the client uses this key to send an `create_account` command to the selected bunker:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": nip04_encrypt("{
|
||||||
|
method: "create_account",
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
email: "<optional-email-to-identify-the-user>",
|
||||||
|
username: "<desired-username>",
|
||||||
|
domain: "<desired-nip05-domain>" // it should be available in this bunker
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* the bunker might reply with an `auth_url` response. The client opens this URL. The client might include a `redirect_uri` parameter where the user should be redirected to.
|
||||||
|
* after signup/client-authorization the client's `create_account` request will be responded with the new user's pubkey or, if a `redirect_uri` was provided, the user will be redirected to the `redirect_uri` with the new user's pubkey as a query string parameter (`pubkey`).
|
||||||
|
* in this screen the client can issue a `connect` NIP-46 request to the user's pubkey to verify that everything is working.
|
||||||
|
|
||||||
|
## NIP-05
|
||||||
|
In the background, the bunker will have configured the requested NIP-05 mapping so that the user can use this nostr address to login next time.
|
||||||
|
|
||||||
|
## NIP-47
|
||||||
|
To complete the experience, allowing new users to have a LN wallet immmediately available is very interesting. The bunker can optionally create an LNBits-backed wallet with zapping capabilities.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nsecbunkerd",
|
"name": "nsecbunkerd",
|
||||||
"version": "0.8.2",
|
"version": "0.9.0",
|
||||||
"description": "nsecbunker daemon",
|
"description": "nsecbunker daemon",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup src/index.ts; tsup src/daemon/index.ts -d dist/daemon; tsup src/client.ts -d dist/client",
|
"build": "tsup src/index.ts; tsup src/daemon/index.ts -d dist/daemon; tsup src/client.ts -d dist/client",
|
||||||
|
"build:client": "tsup src/client.ts -d dist/client",
|
||||||
"prisma:generate": "npx prisma generate",
|
"prisma:generate": "npx prisma generate",
|
||||||
"prisma:migrate": "npx prisma migrate deploy",
|
"prisma:migrate": "npx prisma migrate deploy",
|
||||||
"prisma:create": "npx prisma db push --preview-feature",
|
"prisma:create": "npx prisma db push --preview-feature",
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
"@fastify/view": "^8.2.0",
|
"@fastify/view": "^8.2.0",
|
||||||
"@inquirer/password": "^1.1.2",
|
"@inquirer/password": "^1.1.2",
|
||||||
"@inquirer/prompts": "^1.2.3",
|
"@inquirer/prompts": "^1.2.3",
|
||||||
"@nostr-dev-kit/ndk": "^2.3.0",
|
"@nostr-dev-kit/ndk": "^2.3.1",
|
||||||
"@prisma/client": "^5.4.1",
|
"@prisma/client": "^5.4.1",
|
||||||
"@scure/base": "^1.1.1",
|
"@scure/base": "^1.1.1",
|
||||||
"@types/yargs": "^17.0.24",
|
"@types/yargs": "^17.0.24",
|
||||||
|
|||||||
57
pnpm-lock.yaml
generated
57
pnpm-lock.yaml
generated
@@ -18,8 +18,8 @@ dependencies:
|
|||||||
specifier: ^1.2.3
|
specifier: ^1.2.3
|
||||||
version: 1.2.3
|
version: 1.2.3
|
||||||
'@nostr-dev-kit/ndk':
|
'@nostr-dev-kit/ndk':
|
||||||
specifier: ^2.2.0
|
specifier: ^2.3.1
|
||||||
version: 2.2.0(typescript@5.1.3)
|
version: 2.3.1(typescript@5.1.3)
|
||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^5.4.1
|
specifier: ^5.4.1
|
||||||
version: 5.4.1(prisma@5.4.1)
|
version: 5.4.1(prisma@5.4.1)
|
||||||
@@ -40,10 +40,10 @@ dependencies:
|
|||||||
version: 16.3.1
|
version: 16.3.1
|
||||||
eslint-config-prettier:
|
eslint-config-prettier:
|
||||||
specifier: ^8.8.0
|
specifier: ^8.8.0
|
||||||
version: 8.8.0(eslint@8.55.0)
|
version: 8.8.0(eslint@8.56.0)
|
||||||
eslint-plugin-import:
|
eslint-plugin-import:
|
||||||
specifier: ^2.27.5
|
specifier: ^2.27.5
|
||||||
version: 2.27.5(eslint@8.55.0)
|
version: 2.27.5(eslint@8.56.0)
|
||||||
eventemitter3:
|
eventemitter3:
|
||||||
specifier: ^5.0.1
|
specifier: ^5.0.1
|
||||||
version: 5.0.1
|
version: 5.0.1
|
||||||
@@ -307,13 +307,13 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@eslint-community/eslint-utils@4.4.0(eslint@8.55.0):
|
/@eslint-community/eslint-utils@4.4.0(eslint@8.56.0):
|
||||||
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
|
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.55.0
|
eslint: 8.56.0
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ packages:
|
|||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
espree: 9.6.1
|
espree: 9.6.1
|
||||||
globals: 13.23.0
|
globals: 13.24.0
|
||||||
ignore: 5.3.0
|
ignore: 5.3.0
|
||||||
import-fresh: 3.3.0
|
import-fresh: 3.3.0
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
@@ -339,8 +339,8 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@eslint/js@8.55.0:
|
/@eslint/js@8.56.0:
|
||||||
resolution: {integrity: sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==}
|
resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@@ -582,6 +582,11 @@ packages:
|
|||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@noble/hashes@1.3.3:
|
||||||
|
resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@noble/secp256k1@2.0.0:
|
/@noble/secp256k1@2.0.0:
|
||||||
resolution: {integrity: sha512-rUGBd95e2a45rlmFTqQJYEFA4/gdIARFfuTuTqLglz0PZ6AKyzyXsEZZq7UZn8hZsvaBgpCzKKBJizT2cJERXw==}
|
resolution: {integrity: sha512-rUGBd95e2a45rlmFTqQJYEFA4/gdIARFfuTuTqLglz0PZ6AKyzyXsEZZq7UZn8hZsvaBgpCzKKBJizT2cJERXw==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -604,10 +609,10 @@ packages:
|
|||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.15.0
|
fastq: 1.15.0
|
||||||
|
|
||||||
/@nostr-dev-kit/ndk@2.2.0(typescript@5.1.3):
|
/@nostr-dev-kit/ndk@2.3.1(typescript@5.1.3):
|
||||||
resolution: {integrity: sha512-NdnErX8em9Y/qC4CVYTHYE0bvtLV2ZQh56+JOiarjeJd+J7ZdJX1P10ba463iEOodppvKZqFlYbuDU6CprehUA==}
|
resolution: {integrity: sha512-T9raZyRXJstYWWVIyQIz6dAQVpth+TIda/+OHfuYnxuY6q6Lz5T0jPvue9cMSHrzWmmtUA6yZiJ1PfgzvRn8+g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 1.3.2
|
'@noble/hashes': 1.3.3
|
||||||
'@noble/secp256k1': 2.0.0
|
'@noble/secp256k1': 2.0.0
|
||||||
'@scure/base': 1.1.1
|
'@scure/base': 1.1.1
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
@@ -1365,13 +1370,13 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/eslint-config-prettier@8.8.0(eslint@8.55.0):
|
/eslint-config-prettier@8.8.0(eslint@8.56.0):
|
||||||
resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==}
|
resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>=7.0.0'
|
eslint: '>=7.0.0'
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.55.0
|
eslint: 8.56.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/eslint-import-resolver-node@0.3.7:
|
/eslint-import-resolver-node@0.3.7:
|
||||||
@@ -1384,7 +1389,7 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/eslint-module-utils@2.8.0(eslint-import-resolver-node@0.3.7)(eslint@8.55.0):
|
/eslint-module-utils@2.8.0(eslint-import-resolver-node@0.3.7)(eslint@8.56.0):
|
||||||
resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
|
resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1406,13 +1411,13 @@ packages:
|
|||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
eslint: 8.55.0
|
eslint: 8.56.0
|
||||||
eslint-import-resolver-node: 0.3.7
|
eslint-import-resolver-node: 0.3.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/eslint-plugin-import@2.27.5(eslint@8.55.0):
|
/eslint-plugin-import@2.27.5(eslint@8.56.0):
|
||||||
resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==}
|
resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1427,9 +1432,9 @@ packages:
|
|||||||
array.prototype.flatmap: 1.3.1
|
array.prototype.flatmap: 1.3.1
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 8.55.0
|
eslint: 8.56.0
|
||||||
eslint-import-resolver-node: 0.3.7
|
eslint-import-resolver-node: 0.3.7
|
||||||
eslint-module-utils: 2.8.0(eslint-import-resolver-node@0.3.7)(eslint@8.55.0)
|
eslint-module-utils: 2.8.0(eslint-import-resolver-node@0.3.7)(eslint@8.56.0)
|
||||||
has: 1.0.3
|
has: 1.0.3
|
||||||
is-core-module: 2.12.1
|
is-core-module: 2.12.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -1457,15 +1462,15 @@ packages:
|
|||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/eslint@8.55.0:
|
/eslint@8.56.0:
|
||||||
resolution: {integrity: sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==}
|
resolution: {integrity: sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0)
|
'@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0)
|
||||||
'@eslint-community/regexpp': 4.10.0
|
'@eslint-community/regexpp': 4.10.0
|
||||||
'@eslint/eslintrc': 2.1.4
|
'@eslint/eslintrc': 2.1.4
|
||||||
'@eslint/js': 8.55.0
|
'@eslint/js': 8.56.0
|
||||||
'@humanwhocodes/config-array': 0.11.13
|
'@humanwhocodes/config-array': 0.11.13
|
||||||
'@humanwhocodes/module-importer': 1.0.1
|
'@humanwhocodes/module-importer': 1.0.1
|
||||||
'@nodelib/fs.walk': 1.2.8
|
'@nodelib/fs.walk': 1.2.8
|
||||||
@@ -1485,7 +1490,7 @@ packages:
|
|||||||
file-entry-cache: 6.0.1
|
file-entry-cache: 6.0.1
|
||||||
find-up: 5.0.0
|
find-up: 5.0.0
|
||||||
glob-parent: 6.0.2
|
glob-parent: 6.0.2
|
||||||
globals: 13.23.0
|
globals: 13.24.0
|
||||||
graphemer: 1.4.0
|
graphemer: 1.4.0
|
||||||
ignore: 5.3.0
|
ignore: 5.3.0
|
||||||
imurmurhash: 0.1.4
|
imurmurhash: 0.1.4
|
||||||
@@ -1912,8 +1917,8 @@ packages:
|
|||||||
path-is-absolute: 1.0.1
|
path-is-absolute: 1.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/globals@13.23.0:
|
/globals@13.24.0:
|
||||||
resolution: {integrity: sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==}
|
resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
type-fest: 0.20.2
|
type-fest: 0.20.2
|
||||||
|
|||||||
@@ -3,17 +3,21 @@ import NDK, { NDKUser, NDKEvent, NDKPrivateKeySigner, NDKNip46Signer, NostrEvent
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
const command = process.argv[2];
|
const command = process.argv[2];
|
||||||
const remotePubkey = process.argv[3];
|
let remotePubkey = process.argv[3];
|
||||||
const content = process.argv[4];
|
let content = process.argv[4];
|
||||||
const dontPublish = process.argv.includes('--dont-publish');
|
const dontPublish = process.argv.includes('--dont-publish');
|
||||||
const debug = process.argv.includes('--debug');
|
const debug = process.argv.includes('--debug');
|
||||||
|
let signer: NDKNip46Signer;
|
||||||
|
let ndk: NDK;
|
||||||
|
let remoteUser: NDKUser;
|
||||||
|
|
||||||
if (!command) {
|
if (!command) {
|
||||||
console.log('Usage: node src/client.js <command> <remote-npub> <content> [--dont-publish] [--debug] [--pk <key>]');
|
console.log('Usage: node src/client.js <command> <remote-npub> <content> [--dont-publish] [--debug] [--pk <key>]');
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(`\t<command>: command to run (ping, sign)`);
|
console.log(`\t<command>: command to run (ping, sign)`);
|
||||||
console.log(`\t<remote-npub>: 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 (no need for pubkey or id fields) | or kind:1 content string to sign`);
|
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--debug: enable debug mode');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -24,8 +28,8 @@ async function createNDK(): Promise<NDK> {
|
|||||||
enableOutboxModel: false
|
enableOutboxModel: false
|
||||||
});
|
});
|
||||||
if (debug) {
|
if (debug) {
|
||||||
ndk.pool.on('connect', () => console.log('✅ connected'));
|
ndk.pool.on('relay:connect', () => console.log('✅ connected'));
|
||||||
ndk.pool.on('disconnect', () => console.log('❌ disconnected'));
|
ndk.pool.on('relay:disconnect', () => console.log('❌ disconnected'));
|
||||||
}
|
}
|
||||||
await ndk.connect(5000);
|
await ndk.connect(5000);
|
||||||
|
|
||||||
@@ -59,11 +63,35 @@ function loadPrivateKey(): string | undefined {
|
|||||||
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const remoteUser = new NDKUser({npub: remotePubkey});
|
let remoteUser: NDKUser;
|
||||||
const 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);
|
||||||
|
if (!u) {
|
||||||
|
console.log(`Invalid nip05 ${remotePubkey}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
remoteUser = u;
|
||||||
|
remotePubkey = remoteUser.pubkey;
|
||||||
|
} else {
|
||||||
|
remoteUser = new NDKUser({npub: remotePubkey});
|
||||||
|
}
|
||||||
|
|
||||||
|
ndk = await createNDK();
|
||||||
|
let localSigner: NDKPrivateKeySigner;
|
||||||
|
|
||||||
const pk = loadPrivateKey();
|
const pk = loadPrivateKey();
|
||||||
let localSigner: NDKPrivateKeySigner;
|
|
||||||
|
|
||||||
if (pk) {
|
if (pk) {
|
||||||
localSigner = new NDKPrivateKeySigner(pk);
|
localSigner = new NDKPrivateKeySigner(pk);
|
||||||
@@ -72,7 +100,7 @@ function loadPrivateKey(): string | undefined {
|
|||||||
savePrivateKey(localSigner.privateKey!);
|
savePrivateKey(localSigner.privateKey!);
|
||||||
}
|
}
|
||||||
|
|
||||||
const signer = new NDKNip46Signer(ndk, remoteUser.hexpubkey, localSigner);
|
signer = new NDKNip46Signer(ndk, remoteUser.pubkey, localSigner);
|
||||||
if (debug) console.log(`local pubkey`, (await localSigner.user()).npub);
|
if (debug) console.log(`local pubkey`, (await localSigner.user()).npub);
|
||||||
if (debug) console.log(`remote pubkey`, remotePubkey);
|
if (debug) console.log(`remote pubkey`, remotePubkey);
|
||||||
ndk.signer = signer;
|
ndk.signer = signer;
|
||||||
@@ -81,6 +109,27 @@ function loadPrivateKey(): string | undefined {
|
|||||||
console.log(`Go to ${url} to authorize this request`);
|
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 () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
if (debug) console.log(`waiting for authorization (check your nsecBunker)...`);
|
if (debug) console.log(`waiting for authorization (check your nsecBunker)...`);
|
||||||
@@ -109,8 +158,6 @@ function loadPrivateKey(): string | undefined {
|
|||||||
} as NostrEvent);
|
} as NostrEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
event.pubkey = remoteUser.hexpubkey;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await event.sign();
|
await event.sign();
|
||||||
if (debug) {
|
if (debug) {
|
||||||
@@ -124,4 +171,4 @@ function loadPrivateKey(): string | undefined {
|
|||||||
console.log('sign error', e);
|
console.log('sign error', e);
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
})();
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Hexpubkey, NDKPrivateKeySigner, NDKRpcRequest, NDKUserProfile } from "@nostr-dev-kit/ndk";
|
import { Hexpubkey, NDKKind, NDKPrivateKeySigner, NDKRpcRequest, NDKUserProfile } from "@nostr-dev-kit/ndk";
|
||||||
import AdminInterface from "..";
|
import AdminInterface from "..";
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { setupSkeletonProfile } from "../../lib/profile";
|
import { setupSkeletonProfile } from "../../lib/profile";
|
||||||
@@ -8,7 +8,7 @@ import { allowAllRequestsFromKey } from "../../lib/acl";
|
|||||||
import { requestAuthorization } from "../../authorize";
|
import { requestAuthorization } from "../../authorize";
|
||||||
import prisma from "../../../db";
|
import prisma from "../../../db";
|
||||||
|
|
||||||
export async function validate(currentConfig, email: string, username: string, domain: string) {
|
export async function validate(currentConfig, username: string, domain: string, email?: string) {
|
||||||
if (!username) {
|
if (!username) {
|
||||||
throw new Error('username is required');
|
throw new Error('username is required');
|
||||||
}
|
}
|
||||||
@@ -47,32 +47,82 @@ async function addNip05(currentConfig: IConfig, username: string, domain: string
|
|||||||
writeFileSync(nip05File, JSON.stringify(currentNip05s, null, 2));
|
writeFileSync(nip05File, JSON.stringify(currentNip05s, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function validateUsername(username: string | undefined, domain: string, admin: AdminInterface, req: NDKRpcRequest) {
|
||||||
|
if (!username || username.length === 0) {
|
||||||
|
// create a random username of 10 characters
|
||||||
|
username = Math.random().toString(36).substring(2, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateDomain(domain: string | undefined, admin: AdminInterface, req: NDKRpcRequest) {
|
||||||
|
const availableDomains = (await admin.config()).domains;
|
||||||
|
|
||||||
|
if (!availableDomains || Object.keys(availableDomains).length === 0)
|
||||||
|
throw new Error('no domains available');
|
||||||
|
|
||||||
|
if (!domain || domain.length === 0) domain = Object.keys(availableDomains)[0];
|
||||||
|
|
||||||
|
// check if the domain is available
|
||||||
|
if (!availableDomains[domain]) {
|
||||||
|
throw new Error('domain not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
export default async function createAccount(admin: AdminInterface, req: NDKRpcRequest) {
|
export default async function createAccount(admin: AdminInterface, req: NDKRpcRequest) {
|
||||||
const [ payload ] = req.params as [ string ];
|
let [ username, domain, email ] = req.params as [ string?, string?, string? ];
|
||||||
const { email, username, domain } = JSON.parse(payload);
|
|
||||||
|
try {
|
||||||
|
domain = await validateDomain(domain, admin, req);
|
||||||
|
username = await validateUsername(username, domain, admin, req);
|
||||||
|
} catch (e: any) {
|
||||||
|
admin.rpc.sendResponse(req.id, req.pubkey, "error", NDKKind.NostrConnectAdmin, e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const nip05 = `${username}@${domain}`;
|
const nip05 = `${username}@${domain}`;
|
||||||
if (
|
const payload: string[] = [ username, domain ];
|
||||||
await requestAuthorization(
|
if (email) payload.push(email);
|
||||||
|
|
||||||
|
console.log('requesting authorization', payload);
|
||||||
|
|
||||||
|
const authorizationWithPayload = await requestAuthorization(
|
||||||
admin,
|
admin,
|
||||||
nip05,
|
nip05,
|
||||||
req.pubkey,
|
req.pubkey,
|
||||||
req.id,
|
req.id,
|
||||||
req.method,
|
req.method,
|
||||||
payload
|
JSON.stringify(payload)
|
||||||
)
|
);
|
||||||
) {
|
console.log('authorizationWithPayload', authorizationWithPayload);
|
||||||
console.log('authorized');
|
|
||||||
return createAccountReal(admin, req);
|
if (authorizationWithPayload) {
|
||||||
|
const payload = JSON.parse(authorizationWithPayload);
|
||||||
|
username = payload[0];
|
||||||
|
domain = payload[1];
|
||||||
|
email = payload[2];
|
||||||
|
return createAccountReal(admin, req, username, domain, email);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAccountReal(admin: AdminInterface, req: NDKRpcRequest) {
|
export async function createAccountReal(
|
||||||
|
admin: AdminInterface,
|
||||||
|
req: NDKRpcRequest,
|
||||||
|
username: string,
|
||||||
|
domain: string,
|
||||||
|
email?: string
|
||||||
|
) {
|
||||||
|
// Fetch record since the authorization backend might have changed it
|
||||||
|
|
||||||
|
|
||||||
|
console.log('creating account');
|
||||||
try {
|
try {
|
||||||
const currentConfig = await getCurrentConfig(admin.configFile);
|
const currentConfig = await getCurrentConfig(admin.configFile);
|
||||||
const [ payload ] = req.params as [ string ];
|
|
||||||
const { email, username, domain } = JSON.parse(payload);
|
|
||||||
|
|
||||||
await validate(currentConfig, email, username, domain);
|
await validate(currentConfig, username, domain, email);
|
||||||
|
|
||||||
const nip05 = `${username}@${domain}`;
|
const nip05 = `${username}@${domain}`;
|
||||||
const key = NDKPrivateKeySigner.generate();
|
const key = NDKPrivateKeySigner.generate();
|
||||||
@@ -91,8 +141,6 @@ export async function createAccountReal(admin: AdminInterface, req: NDKRpcReques
|
|||||||
const nsec = nip19.nsecEncode(key.privateKey!);
|
const nsec = nip19.nsecEncode(key.privateKey!);
|
||||||
currentConfig.keys[keyName] = { key: key.privateKey };
|
currentConfig.keys[keyName] = { key: key.privateKey };
|
||||||
|
|
||||||
console.log('saving new key', {keyName, privateKey: key.privateKey});
|
|
||||||
|
|
||||||
saveCurrentConfig(admin.configFile, currentConfig);
|
saveCurrentConfig(admin.configFile, currentConfig);
|
||||||
|
|
||||||
await admin.loadNsec!(keyName, nsec);
|
await admin.loadNsec!(keyName, nsec);
|
||||||
@@ -102,16 +150,11 @@ export async function createAccountReal(admin: AdminInterface, req: NDKRpcReques
|
|||||||
// Immediately grant access to the creator key
|
// Immediately grant access to the creator key
|
||||||
await grantPermissions(req, keyName);
|
await grantPermissions(req, keyName);
|
||||||
|
|
||||||
return admin.rpc.sendResponse(req.id, req.pubkey, JSON.stringify([
|
return admin.rpc.sendResponse(req.id, req.pubkey, generatedUser.pubkey, NDKKind.NostrConnectAdmin);
|
||||||
generatedUser.pubkey,
|
|
||||||
|
|
||||||
]));
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.trace('error', e);
|
console.trace('error', e);
|
||||||
return admin.rpc.sendResponse(req.id, req.pubkey, JSON.stringify([
|
return admin.rpc.sendResponse(req.id, req.pubkey, "error", NDKKind.NostrConnectAdmin,
|
||||||
"error",
|
e.message);
|
||||||
e.message
|
|
||||||
]), 24134);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import "websocket-polyfill";
|
import "websocket-polyfill";
|
||||||
import NDK, { NDKEvent, NDKPrivateKeySigner, NDKRpcRequest, NDKRpcResponse, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk';
|
import NDK, { NDKEvent, NDKKind, NDKPrivateKeySigner, NDKRpcRequest, NDKRpcResponse, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk';
|
||||||
import { NDKNostrRpc } from '@nostr-dev-kit/ndk';
|
import { NDKNostrRpc } from '@nostr-dev-kit/ndk';
|
||||||
import { debug } from 'debug';
|
import { debug } from 'debug';
|
||||||
import { Key, KeyUser } from '../run';
|
import { Key, KeyUser } from '../run';
|
||||||
@@ -106,8 +106,8 @@ class AdminInterface {
|
|||||||
this.ndk.connect(2500).then(() => {
|
this.ndk.connect(2500).then(() => {
|
||||||
// connect for whitelisted admins
|
// connect for whitelisted admins
|
||||||
this.rpc.subscribe({
|
this.rpc.subscribe({
|
||||||
"kinds": [24134 as number],
|
"kinds": [NDKKind.NostrConnect, 24134 as number],
|
||||||
"#p": [this.signerUser!.pubkey],
|
"#p": [this.signerUser!.pubkey]
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rpc.on('request', (req) => this.handleRequest(req));
|
this.rpc.on('request', (req) => this.handleRequest(req));
|
||||||
@@ -146,12 +146,13 @@ class AdminInterface {
|
|||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`Error handling request ${req.method}: ${err.message}`, req.params);
|
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);
|
return this.rpc.sendResponse(req.id, req.pubkey, "error", NDKKind.NostrConnectAdmin, err?.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateRequest(req: NDKRpcRequest): Promise<void> {
|
private async validateRequest(req: NDKRpcRequest): Promise<void> {
|
||||||
// if this request is of type create_account, allow it
|
// if this request is of type create_account, allow it
|
||||||
|
// TODO: require some POW to prevent spam
|
||||||
if (req.method === 'create_account' && allowNewKeys) {
|
if (req.method === 'create_account' && allowNewKeys) {
|
||||||
console.log(`allowing create_account request`);
|
console.log(`allowing create_account request`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -28,18 +28,23 @@ export async function requestAuthorization(
|
|||||||
console.log('baseUrl', baseUrl);
|
console.log('baseUrl', baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<boolean>((resolve) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
if (baseUrl) {
|
if (baseUrl) {
|
||||||
// If we have a URL, request authorization through web
|
// If we have a URL, request authorization through web
|
||||||
urlAuthFlow(baseUrl, admin, remotePubkey, requestId, request, resolve);
|
urlAuthFlow(baseUrl, admin, remotePubkey, requestId, request, resolve, reject);
|
||||||
}
|
}
|
||||||
adminAuthFlow(admin, keyName, remotePubkey, method, param, resolve);
|
adminAuthFlow(admin, keyName, remotePubkey, method, param, resolve, reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function adminAuthFlow(adminInterface, keyName, remotePubkey, method, param, resolve) {
|
async function adminAuthFlow(adminInterface, keyName, remotePubkey, method, param, resolve, reject) {
|
||||||
const requestedPerm = await adminInterface.requestPermission(keyName, remotePubkey, method, param);
|
const requestedPerm = await adminInterface.requestPermission(keyName, remotePubkey, method, param);
|
||||||
return requestedPerm;
|
|
||||||
|
if (requestedPerm) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createRecord(
|
async function createRecord(
|
||||||
@@ -82,12 +87,11 @@ export function urlAuthFlow(
|
|||||||
remotePubkey: Hexpubkey,
|
remotePubkey: Hexpubkey,
|
||||||
requestId: string,
|
requestId: string,
|
||||||
request: Request,
|
request: Request,
|
||||||
resolve: any
|
resolve: any,
|
||||||
|
reject: any
|
||||||
) {
|
) {
|
||||||
const url = generatePendingAuthUrl(baseUrl, request);
|
const url = generatePendingAuthUrl(baseUrl, request);
|
||||||
|
|
||||||
console.log({url});
|
|
||||||
|
|
||||||
admin.rpc.sendResponse(requestId, remotePubkey, "auth_url", undefined, url);
|
admin.rpc.sendResponse(requestId, remotePubkey, "auth_url", undefined, url);
|
||||||
|
|
||||||
// Regularly poll to see if this request was approved so we can synchronously resolve
|
// Regularly poll to see if this request was approved so we can synchronously resolve
|
||||||
@@ -106,7 +110,11 @@ export function urlAuthFlow(
|
|||||||
|
|
||||||
if (record.allowed !== undefined && record.allowed !== null) {
|
if (record.allowed !== undefined && record.allowed !== null) {
|
||||||
clearInterval(checkingInterval);
|
clearInterval(checkingInterval);
|
||||||
resolve(!!record.allowed);
|
|
||||||
|
if (record.allowed === false) {
|
||||||
|
reject(record.payload);
|
||||||
|
}
|
||||||
|
resolve(record.params);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ export async function authorizeRequestWebHandler(request, reply) {
|
|||||||
|
|
||||||
if (method === "create_account") {
|
if (method === "create_account") {
|
||||||
const payload = JSON.parse(record.params);
|
const payload = JSON.parse(record.params);
|
||||||
console.log({payload});
|
const [ username, domain, email ] = payload;
|
||||||
email = payload.email;
|
|
||||||
username = payload.username;
|
|
||||||
domain = payload.domain;
|
|
||||||
nip05 = `${username}@${domain}`;
|
nip05 = `${username}@${domain}`;
|
||||||
|
|
||||||
return reply.view("/templates/createAccount.handlebar", { record, email, username, domain, nip05, callbackUrl });
|
return reply.view("/templates/createAccount.handlebar", { record, email, username, domain, nip05, callbackUrl });
|
||||||
@@ -74,14 +71,27 @@ export async function processRegistrationWebHandler(request, reply) {
|
|||||||
const record = await prisma.request.findUnique({
|
const record = await prisma.request.findUnique({
|
||||||
where: { id: request.params.id }
|
where: { id: request.params.id }
|
||||||
});
|
});
|
||||||
|
const body = request.body;
|
||||||
|
|
||||||
if (!record || record.allowed) {
|
if (!record || record.allowed) {
|
||||||
return { ok: false, error: "Request not found or already processed" };
|
return { ok: false, error: "Request not found or already processed" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we serialize the payload again and store it
|
||||||
|
// along with the allowed flag
|
||||||
|
// so that the original caller can get the current state
|
||||||
|
// to be processed
|
||||||
|
const payload: string[] = [];
|
||||||
|
payload.push(body.username);
|
||||||
|
payload.push(body.domain);
|
||||||
|
payload.push(body.email);
|
||||||
|
payload.push(body.password);
|
||||||
|
|
||||||
|
// TODO: validations here
|
||||||
|
|
||||||
await prisma.request.update({
|
await prisma.request.update({
|
||||||
where: { id: request.params.id },
|
where: { id: request.params.id },
|
||||||
data: { allowed: true }
|
data: { params: JSON.stringify(payload), allowed: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
let createdPubkey: string | undefined;
|
let createdPubkey: string | undefined;
|
||||||
@@ -100,7 +110,6 @@ export async function processRegistrationWebHandler(request, reply) {
|
|||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
const body = request.body;
|
|
||||||
const callbackUrlString = body.callbackUrl;
|
const callbackUrlString = body.callbackUrl;
|
||||||
let callbackUrl: string | undefined;
|
let callbackUrl: string | undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -51,14 +51,15 @@
|
|||||||
<span class="text-gray-500 text-sm absolute right-4">
|
<span class="text-gray-500 text-sm absolute right-4">
|
||||||
@{{domain}}
|
@{{domain}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<input type="hidden" name="domain" value="{{domain}}" />
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span>Password</span>
|
<span>Password</span>
|
||||||
<input type="password" name="password" placeholder="Enter a Password" class="w-full px-4 py-3 bg-white rounded-lg rounded-b-none shadow border border-neutral-200 justify-start items-center gap-2 inline-flex" />
|
<input type="password" name="password" placeholder="Enter a Password" class="w-full px-4 py-3 bg-white rounded-lg rounded-b-none shadow border border-neutral-200 justify-start items-center gap-2 inline-flex" />
|
||||||
<input type="password" name="password" placeholder="Password confirmation" class="w-full px-4 py-3 bg-white rounded-lg rounded-t-none shadow border border-neutral-200 justify-start items-center gap-2 inline-flex" />
|
<input type="password" name="confirm_password" placeholder="Password confirmation" class="w-full px-4 py-3 bg-white rounded-lg rounded-t-none shadow border border-neutral-200 justify-start items-center gap-2 inline-flex" />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="hidden" name="callbackUrl" value="{{callbackUrl}}" />
|
<input type="hidden" name="callbackUrl" value="{{callbackUrl}}" />
|
||||||
|
|||||||
Reference in New Issue
Block a user