dwnpm
Version:
Decentralized Registry Package Manager (DRPM) helps developers publish, install, find and manage Decentralized Packages (DPKs) published to Decentralized Web Nodes (DWNs). DRPM does this by looking up a Decentralized Identifier (DID) to find its DID docum
213 lines • 8.93 kB
JavaScript
import * as Inquirer from '@inquirer/prompts';
import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from 'crypto';
import { readFileSync, writeFileSync } from 'fs';
import { ensureDir } from 'fs-extra';
import { readFile, writeFile } from 'fs/promises';
import { DEFAULT_PASSWORD, DEFAULT_PROFILE, DRPM_HOME, DRPM_PROFILE } from '../config.js';
import { Setup } from '../lib/setup.js';
import formatter from '../utils/formatter.js';
import { Logger } from '../utils/logger.js';
import { createPassword, secureProfile, secureProfileContext, stringifier } from '../utils/misc.js';
import { Context } from './context.js';
export class Profile {
static json;
context;
constructor(name) {
if (!this.exists()) {
Logger.error('ProfileError: No profile found.');
this.template();
}
if (!this.isSetup()) {
Logger.error('ProfileError: Setup not completed.');
// TODO: run setup? await Setup.run();
// process.exit(1);
}
name ??= Profile.loadStaticSync()?.name;
this.context = new Context(name, Profile.json?.[name]);
}
isSetup() {
return Setup.isDone();
}
static loadStaticSync() {
const profile = readFileSync(DRPM_PROFILE, 'utf8');
this.json = JSON.parse(profile);
return this.json;
}
template() {
writeFileSync(DRPM_PROFILE, stringifier(DEFAULT_PROFILE));
}
// Helper function to validate profile data
valid(data) {
if (!data) {
Logger.error('ProfileError: No profile data found.');
return false;
}
const { did, password, dwnEndpoints, web5DataPath } = data?.[data?.name] ?? {};
// Check for empty or invalid DID
if (!did || did.trim() === '') {
Logger.error('ProfileError: DID cannot be blank.');
return false;
}
// Check for empty or default password
if (!password || password === DEFAULT_PASSWORD) {
Logger.error('ProfileError: Password cannot be blank or set to the default password.');
return false;
}
// Check that dwnEndpoint has at least one valid entry
if (!Array.isArray(dwnEndpoints) || dwnEndpoints.length === 0) {
Logger.error('ProfileError: DWN endpoint cannot be empty.');
return false;
}
// Check if dataPath is empty or invalid
if (!web5DataPath || web5DataPath.trim() === '') {
Logger.error('ProfileError: Web5 Data Path cannot be empty.');
return false;
}
// If no errors, return the profile
return true;
}
// Helper function to check if a profile exists
exists(profile, method) {
try {
profile ??= Profile.loadStaticSync();
if (!profile)
return false;
const data = profile[profile.name ?? method];
if (!data)
return false;
return profile;
}
catch (error) {
Logger.error('ProfileCommand: Failed to load profile', error);
return false;
}
}
// Helper function to load existing profile or create a new one
async load() {
const profile = await readFile(DRPM_PROFILE, 'utf8');
Profile.json = JSON.parse(profile);
return Profile.json;
}
static async staticSave() {
const profile = Profile.json ?? this.loadStaticSync();
await writeFile(DRPM_PROFILE, stringifier(profile), 'utf8');
Logger.log('Saved profile.json', secureProfile(Profile.json));
}
async save() {
const profile = Profile.json ?? this.load();
await writeFile(DRPM_PROFILE, stringifier(profile), 'utf8');
Logger.log('Saved profile.json', secureProfile(Profile.json));
}
async read(options) {
Logger.plain('DRPM Profile:', options.text ?? false ? Profile.json : secureProfile(Profile.json));
}
async add(options) {
// TODO: implement add
throw new Error('ProfileCommand: add not implemented: ' + options);
}
encrypt({ password }) {
password ??= createPassword();
const jsonString = JSON.stringify(Profile.json);
const salt = randomBytes(16);
const iv = randomBytes(12);
const key = pbkdf2Sync(password, salt, 100000, 32, 'sha256');
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(jsonString, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
return {
data: encrypted,
salt: salt.toString('hex'),
iv: iv.toString('hex'),
authTag: authTag
};
}
// Function to decrypt JSON
async delete({ password, force }) {
password ??= createPassword();
const answer = force
? 'Force'
: await Inquirer.select({
choices: ['Yes', 'No'],
message: 'Are you sure you want to delete profile.json?'
}) ?? 'No';
if (answer === 'No') {
Logger.log('Cancelling deletion');
return process.exit(0);
}
if (answer !== 'Force') {
const encrypted = this.encrypt({ password });
const encryptedFilepath = `${DRPM_HOME}/profile-${encrypted.authTag}.enc`;
await writeFile(encryptedFilepath, stringifier(encrypted));
Logger.log(`Created encrypted backup at ${encryptedFilepath}`);
}
else {
Logger.log('Creating encrypted backup of profile.json');
await writeFile(DRPM_PROFILE, stringifier(DEFAULT_PROFILE));
Logger.log('Deleted all profiles!');
}
}
async list() {
Logger.plain(`Available Profile Contexts:\n${Object.keys(Profile.json)
.filter((key) => key !== 'name')
.map((key, i) => ` ${i + 1}. ${key.trim()} (${key === Profile.json.name ? formatter.green('active') : formatter.red('inactive')})`).join('\n')}`);
}
// Subcommand function to switch between profiles
async switch({ name }) {
if (name) {
Profile.json.name = name;
}
else {
const choices = Object.keys(Profile.json)
.filter((key) => key !== 'name')
.map((key) => `${key.trim()} (${key === Profile.json.name ? 'active' : 'inactive'})`);
const name = await Inquirer.select({ choices, message: 'Which profile context would you like to switch to?' });
Profile.json.name = name.replace(/ \(.*\)/, '');
}
await this.save();
const profile = Profile.json;
const activeName = Profile.json.name;
const context = profile[activeName];
Logger.log(`Switched profile context to ${activeName}: ${secureProfileContext(context)}`);
}
static async recover({ password, file }) {
const profileBackup = JSON.parse(await readFile(file, 'utf8'));
const { data, salt, iv, authTag } = profileBackup ?? {};
if (!(data && salt && iv && authTag)) {
Logger.error(`ProfileError: Invalid backup file ${file}`);
process.exit(1);
}
const key = pbkdf2Sync(password, Buffer.from(salt, 'hex'), 100000, 32, 'sha256');
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'hex'));
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
Profile.json = JSON.parse(decrypted);
await this.staticSave();
Logger.log(`Recovered profile.json from backup ${file}`);
}
async backup({ password }) {
const writePassword = !password;
password ??= createPassword();
const jsonString = JSON.stringify(Profile.json);
const salt = randomBytes(16);
const iv = randomBytes(12);
const key = pbkdf2Sync(password, salt, 100000, 32, 'sha256');
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encryptedData = cipher.update(jsonString, 'utf8', 'hex');
encryptedData += cipher.final('hex');
const encrypted = {
data: encryptedData,
salt: salt.toString('hex'),
iv: iv.toString('hex'),
authTag: cipher.getAuthTag().toString('hex')
};
const encryptedDir = `${DRPM_HOME}/bak/${encrypted.authTag}`;
await ensureDir(encryptedDir);
if (writePassword)
await writeFile(`${encryptedDir}/profile.key`, password, 'utf8');
await writeFile(`${encryptedDir}/profile.enc`, stringifier(encrypted), 'utf8');
Logger.log(`Backed up profile.json to ${encryptedDir}`, encrypted);
}
}
//# sourceMappingURL=profile.js.map