mirror of
https://github.com/kind-0/nsecbunkerd.git
synced 2025-07-12 13:12:23 +02:00
updates
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@ -4,10 +4,4 @@ dist
|
||||
**/*.d.ts
|
||||
**/*.d.ts.map
|
||||
nsecbunker.json
|
||||
# Environment variables declared in this file are automatically made available to Prisma.
|
||||
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
|
||||
|
||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||
|
||||
DATABASE_URL="file:./dev.db"
|
||||
prisma/nsecbunker.db*
|
||||
|
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
FROM --platform=linux/amd64 node:19 as build
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json .
|
||||
RUN npm i
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
RUN npx prisma generate
|
||||
ENTRYPOINT [ "node", "dist/index.js" ]
|
||||
CMD ["start"]
|
20
LICENSE
Normal file
20
LICENSE
Normal file
@ -0,0 +1,20 @@
|
||||
Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0) License
|
||||
|
||||
Copyright (c) 2023 Sanity Island, Inc.
|
||||
|
||||
This package is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
|
||||
|
||||
You are free to:
|
||||
|
||||
- Share: Copy and redistribute the material in any medium or format.
|
||||
|
||||
Under the following terms:
|
||||
|
||||
- Attribution: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
|
||||
|
||||
- NonCommercial: You may not use the material for commercial purposes.
|
||||
|
||||
- NoDerivatives: If you remix, transform, or build upon the material, you may not distribute the modified material.
|
||||
|
||||
For full legal details of this license, please visit the Creative Commons website at https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode.
|
||||
|
31
SECURITY-MODEL.md
Normal file
31
SECURITY-MODEL.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Security Model
|
||||
|
||||
The premise of nsecBunker is that you can store Nostr private keys (nsecs), use them remotely
|
||||
under certain policies, but these keys can never be exfiltrated from nsecBunker.
|
||||
|
||||
All communication with nsecBunker happens through encrypted, ephemeral nostr events.
|
||||
|
||||
## Keys
|
||||
Within nsecBunker there are two distinct sets of keys:
|
||||
### User keys
|
||||
The keys that users want to sign with (e.g. your personal or company's key).
|
||||
|
||||
These keys are stored encrypted with a passphrase; the same way Lightning Network's LND
|
||||
stores keys locally: every time you start nsecBunker, you must enter the passphrase to decrypt it.
|
||||
Without this passphrase, keys cannot be used.
|
||||
|
||||
### nsecBunker's key
|
||||
nsecBunker generates it's own private key, which is used solely to communicate
|
||||
with the nsecBunker administration UI. If these keys are compromised, no key material is at risk.
|
||||
|
||||
To interact with nsecBunker's administration UI, the administrator(s)' keys must be whitelisted
|
||||
within nsecBunker. All communication between the administrator and the nsecBunker is end-to-end
|
||||
encrypted with these two set of keys.
|
||||
|
||||
Non-whitelisted keys simply cannot talk to nsecBunker's Administration UI, which is why even if
|
||||
the nsecBunker connection string that is created when you setup your nsecBunker is leaked, nothing
|
||||
happens.
|
||||
|
||||
## Nostr Connect
|
||||
nsecBunker listens on certain relays (specified in the config file) for keys that are attempting to
|
||||
sign with the
|
2946
pnpm-lock.yaml
generated
Normal file
2946
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
prisma/migrations/20230527214402_add_timestamp/migration.sql
Normal file
27
prisma/migrations/20230527214402_add_timestamp/migration.sql
Normal file
@ -0,0 +1,27 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Log" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"timestamp" DATETIME NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"method" TEXT,
|
||||
"params" TEXT,
|
||||
"keyUserId" INTEGER,
|
||||
CONSTRAINT "Log_keyUserId_fkey" FOREIGN KEY ("keyUserId") REFERENCES "KeyUser" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_KeyUser" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"keyName" TEXT NOT NULL,
|
||||
"userPubkey" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastUsedAt" DATETIME
|
||||
);
|
||||
INSERT INTO "new_KeyUser" ("id", "keyName", "userPubkey") SELECT "id", "keyName", "userPubkey" FROM "KeyUser";
|
||||
DROP TABLE "KeyUser";
|
||||
ALTER TABLE "new_KeyUser" RENAME TO "KeyUser";
|
||||
CREATE UNIQUE INDEX "KeyUser_keyName_userPubkey_key" ON "KeyUser"("keyName", "userPubkey");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
2
prisma/migrations/20230527215212_add_descr/migration.sql
Normal file
2
prisma/migrations/20230527215212_add_descr/migration.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "KeyUser" ADD COLUMN "description" TEXT;
|
@ -0,0 +1,17 @@
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_SigningCondition" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"method" TEXT,
|
||||
"kind" TEXT,
|
||||
"content" TEXT,
|
||||
"keyUserKeyName" TEXT,
|
||||
"allowed" BOOLEAN,
|
||||
"keyUserId" INTEGER,
|
||||
CONSTRAINT "SigningCondition_keyUserId_fkey" FOREIGN KEY ("keyUserId") REFERENCES "KeyUser" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_SigningCondition" ("allowed", "content", "id", "keyUserId", "keyUserKeyName", "kind", "method") SELECT "allowed", "content", "id", "keyUserId", "keyUserKeyName", "kind", "method" FROM "SigningCondition";
|
||||
DROP TABLE "SigningCondition";
|
||||
ALTER TABLE "new_SigningCondition" RENAME TO "SigningCondition";
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
37
src/daemon/admin/commands/create_new_key.ts
Normal file
37
src/daemon/admin/commands/create_new_key.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { NDKPrivateKeySigner, NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
import { saveEncrypted } from "../../../commands/add.js";
|
||||
import { nip19 } from 'nostr-tools';
|
||||
|
||||
export default async function createNewKey(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [ keyName, passphrase, _nsec ] = req.params as [ string, string, string? ];
|
||||
|
||||
if (!keyName || !passphrase) throw new Error("Invalid params");
|
||||
if (!admin.loadNsec) throw new Error("No unlockKey method");
|
||||
|
||||
let key;
|
||||
|
||||
if (_nsec) {
|
||||
key = new NDKPrivateKeySigner(nip19.decode(_nsec).data as string);
|
||||
} else {
|
||||
key = NDKPrivateKeySigner.generate();
|
||||
}
|
||||
|
||||
const user = await key.user();
|
||||
const nsec = nip19.nsecEncode(key.privateKey!);
|
||||
|
||||
await saveEncrypted(
|
||||
admin.configFile,
|
||||
nsec,
|
||||
passphrase,
|
||||
keyName
|
||||
);
|
||||
|
||||
await admin.loadNsec(keyName, nsec);
|
||||
|
||||
const result = JSON.stringify({
|
||||
npub: user.npub,
|
||||
});
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, 24134);
|
||||
}
|
20
src/daemon/admin/commands/unlock_key.ts
Normal file
20
src/daemon/admin/commands/unlock_key.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { NDKRpcRequest } from "@nostr-dev-kit/ndk";
|
||||
import AdminInterface from "../index.js";
|
||||
|
||||
export default async function unlockKey(admin: AdminInterface, req: NDKRpcRequest) {
|
||||
const [ keyName, passphrase ] = req.params as [ string, string ];
|
||||
|
||||
if (!keyName || !passphrase) throw new Error("Invalid params");
|
||||
if (!admin.unlockKey) throw new Error("No unlockKey method");
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
const res = await admin.unlockKey(keyName, passphrase);
|
||||
result = JSON.stringify({ success: res });
|
||||
} catch (e: any) {
|
||||
result = JSON.stringify({ success: false, error: e.message });
|
||||
}
|
||||
|
||||
return admin.rpc.sendResponse(req.id, req.pubkey, result, 24134);
|
||||
}
|
218
src/daemon/admin/index.ts
Normal file
218
src/daemon/admin/index.ts
Normal file
@ -0,0 +1,218 @@
|
||||
import NDK, { NDKEvent, NDKPrivateKeySigner, NDKRpcRequest, NDKRpcResponse, NDKUser } from '@nostr-dev-kit/ndk';
|
||||
import { NDKNostrRpc } from '@nostr-dev-kit/ndk';
|
||||
import { debug } from 'debug';
|
||||
import { Key, KeyUser } from '../run';
|
||||
import {
|
||||
checkIfPubkeyAllowed,
|
||||
allowAllRequestsFromKey,
|
||||
rejectAllRequestsFromKey
|
||||
} from '../lib/acl/index.js';
|
||||
import prisma from '../../db';
|
||||
import createNewKey from './commands/create_new_key';
|
||||
import unlockKey from './commands/unlock_key';
|
||||
|
||||
export type IAdminOpts = {
|
||||
npubs: string[];
|
||||
adminRelays: string[];
|
||||
key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents the admin interface for the nsecbunker daemon.
|
||||
*
|
||||
* It provides an interface for a UI to manage the daemon over nostr.
|
||||
*/
|
||||
class AdminInterface {
|
||||
private npubs: string[];
|
||||
private ndk: NDK;
|
||||
private signerUser?: NDKUser;
|
||||
readonly rpc: NDKNostrRpc;
|
||||
readonly configFile: string;
|
||||
public getKeys?: () => Promise<Key[]>;
|
||||
public getKeyUsers?: (req: NDKRpcRequest) => Promise<KeyUser[]>;
|
||||
public unlockKey?: (keyName: string, passphrase: string) => Promise<boolean>;
|
||||
public loadNsec?: (keyName: string, nsec: string) => void;
|
||||
|
||||
constructor(opts: IAdminOpts, configFile: string) {
|
||||
this.configFile = configFile;
|
||||
this.npubs = opts.npubs;
|
||||
this.ndk = new NDK({
|
||||
explicitRelayUrls: opts.adminRelays,
|
||||
signer: new NDKPrivateKeySigner(opts.key),
|
||||
});
|
||||
this.ndk.signer?.user().then((user: NDKUser) => {
|
||||
let connectionString = `bunker://${user.npub}`;
|
||||
|
||||
if (opts.adminRelays.length > 0) {
|
||||
connectionString += `@${opts.adminRelays.join(',').replace(/wss:\/\//g, '')}`;
|
||||
}
|
||||
|
||||
console.log(`\n\nnsecBunker connection string:\n\n${connectionString}\n\n`);
|
||||
|
||||
this.signerUser = user;
|
||||
|
||||
this.connect();
|
||||
});
|
||||
|
||||
this.rpc = new NDKNostrRpc(this.ndk, this.ndk.signer!, debug("ndk:rpc"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the npub of the admin interface.
|
||||
*/
|
||||
public async npub() {
|
||||
return (await this.ndk.signer?.user())!.npub;
|
||||
}
|
||||
|
||||
private connect() {
|
||||
if (this.npubs.length <= 0) {
|
||||
console.log(`❌ Admin interface not starting because no admin npubs were provided`);
|
||||
return;
|
||||
}
|
||||
|
||||
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(() => {
|
||||
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));
|
||||
}).catch((err) => {
|
||||
console.log('❌ admin connection failed');
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleRequest(req: NDKRpcRequest) {
|
||||
// await this.validateRequest(req);
|
||||
|
||||
switch (req.method) {
|
||||
case 'get_keys': this.reqGetKeys(req); break;
|
||||
case 'get_key_users': this.reqGetKeyUsers(req); break;
|
||||
case 'create_new_key': createNewKey(this, req); break;
|
||||
case 'unlock_key': unlockKey(this, req); break;
|
||||
default:
|
||||
console.log(`Unknown method ${req.method}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async validateRequest(req: NDKRpcRequest) {
|
||||
// TODO validate pubkey, validate signature
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to fetch keys and their current state
|
||||
*/
|
||||
private async reqGetKeys(req: NDKRpcRequest) {
|
||||
if (!this.getKeys) throw new Error('getKeys() not implemented');
|
||||
|
||||
const result = JSON.stringify(await this.getKeys());
|
||||
const pubkey = req.pubkey;
|
||||
|
||||
return this.rpc.sendResponse(req.id, pubkey, result, 24134); // 24134
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to fetch users of a key
|
||||
*/
|
||||
private async reqGetKeyUsers(req: NDKRpcRequest): Promise<void> {
|
||||
if (!this.getKeyUsers) throw new Error('getKeyUsers() not implemented');
|
||||
|
||||
const result = JSON.stringify(await this.getKeyUsers(req));
|
||||
const pubkey = req.pubkey;
|
||||
|
||||
return this.rpc.sendResponse(req.id, pubkey, result, 24134); // 24134
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This function is called when a request is received from a remote user that needs
|
||||
* to be approved by the admin interface.
|
||||
*/
|
||||
public async requestPermission(
|
||||
keyName: string,
|
||||
remotePubkey: string,
|
||||
method: string,
|
||||
param: any
|
||||
): Promise<boolean> {
|
||||
const keyUser = await prisma.keyUser.findUnique({
|
||||
where: {
|
||||
unique_key_user: {
|
||||
keyName,
|
||||
userPubkey: remotePubkey,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (method === 'sign_event') {
|
||||
const e = param.rawEvent();
|
||||
param = JSON.stringify(e);
|
||||
|
||||
console.log(`👀 Event to be signed\n`, {
|
||||
kind: e.kind,
|
||||
content: e.content,
|
||||
tags: e.tags,
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`requesting permission for`, keyName);
|
||||
console.log(`remotePubkey`, remotePubkey);
|
||||
console.log(`method`, method);
|
||||
console.log(`param`, param);
|
||||
console.log(`keyUser`, keyUser);
|
||||
|
||||
for (const npub of this.npubs) {
|
||||
const remoteUser = new NDKUser({npub});
|
||||
console.log(`sending request to ${npub}`, remoteUser.hexpubkey());
|
||||
this.rpc.sendRequest(
|
||||
remoteUser.hexpubkey(),
|
||||
'acl',
|
||||
[JSON.stringify({
|
||||
keyName,
|
||||
remotePubkey,
|
||||
method,
|
||||
param,
|
||||
description: keyUser?.description,
|
||||
})],
|
||||
24134, // 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminInterface;
|
140
src/daemon/lib/acl/index.ts
Normal file
140
src/daemon/lib/acl/index.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import prisma from '../../../db.js';
|
||||
|
||||
export async function checkIfPubkeyAllowed(
|
||||
keyName: string,
|
||||
remotePubkey: string,
|
||||
method: string,
|
||||
event?: NDKEvent
|
||||
): Promise<boolean | undefined> {
|
||||
// find KeyUser
|
||||
const keyUser = await prisma.keyUser.findUnique({
|
||||
where: { unique_key_user: { keyName, userPubkey: remotePubkey } },
|
||||
});
|
||||
|
||||
if (!keyUser) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// find SigningCondition
|
||||
const signingConditionQuery = requestToSigningConditionQuery(method, event);
|
||||
|
||||
const explicitReject = await prisma.signingCondition.findFirst({
|
||||
where: {
|
||||
keyUserId: keyUser.id,
|
||||
method: '*',
|
||||
allowed: false,
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export type IAllowScope = {
|
||||
kind?: number | 'all';
|
||||
};
|
||||
|
||||
export function requestToSigningConditionQuery(method: string, event?: NDKEvent) {
|
||||
const signingConditionQuery: any = { method };
|
||||
|
||||
switch (method) {
|
||||
case 'sign_event':
|
||||
signingConditionQuery.kind = event?.kind?.toString();
|
||||
break;
|
||||
}
|
||||
|
||||
return signingConditionQuery;
|
||||
}
|
||||
|
||||
export function allowScopeToSigningConditionQuery(method: string, scope?: IAllowScope) {
|
||||
const signingConditionQuery: any = { method };
|
||||
|
||||
if (scope && scope.kind) {
|
||||
signingConditionQuery.kind = scope.kind.toString();
|
||||
}
|
||||
|
||||
return signingConditionQuery;
|
||||
}
|
||||
|
||||
export async function allowAllRequestsFromKey(
|
||||
remotePubkey: string,
|
||||
keyName: string,
|
||||
method: string,
|
||||
param?: any,
|
||||
description?: string,
|
||||
allowScope?: IAllowScope,
|
||||
): 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, description },
|
||||
});
|
||||
|
||||
console.log({ upsertedUser });
|
||||
|
||||
// Create a new SigningCondition for the given KeyUser and set allowed to true
|
||||
const signingConditionQuery = allowScopeToSigningConditionQuery(method, allowScope);
|
||||
await prisma.signingCondition.create({
|
||||
data: {
|
||||
allowed: true,
|
||||
keyUserId: upsertedUser.id,
|
||||
...signingConditionQuery
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`create`, {
|
||||
allowed: true,
|
||||
keyUserId: upsertedUser.id,
|
||||
...signingConditionQuery
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.log('allowAllRequestsFromKey', e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function rejectAllRequestsFromKey(remotePubkey: string, keyName: string): Promise<void> {
|
||||
// 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,
|
||||
},
|
||||
});
|
||||
}
|
72
src/utils/prompts/boolean.ts
Normal file
72
src/utils/prompts/boolean.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import readline from 'readline';
|
||||
|
||||
export interface IAskYNquestionOpts {
|
||||
timeoutLength?: number;
|
||||
yes: any;
|
||||
no: any;
|
||||
always?: any;
|
||||
never?: any;
|
||||
response?: any;
|
||||
timeout?: any;
|
||||
}
|
||||
|
||||
export 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':
|
||||
case 'a':
|
||||
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;
|
||||
}
|
Reference in New Issue
Block a user