From e4208a0414d74c2d03d8deddaf933b48853f2227 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sun, 28 Jul 2024 20:44:43 +0200 Subject: [PATCH] feat: optional encode config (#131) * feat: encrypt config * fix: recursive module * feat: migrate config * fix: import * chore: prod build * chore: text * chore: no dev * feat: init config * chore: text * chore: more generic text --- src/commands/auth.ts | 4 +- src/commands/config.ts | 2 +- src/commands/deploy.ts | 2 +- src/commands/init.ts | 15 +- src/commands/open.ts | 2 +- src/commands/use.ts | 20 +- src/commands/version.ts | 14 +- src/commands/whoami.ts | 8 +- src/configs/cli.config.ts | 198 ++++++++++-------- src/configs/cli.settings.config.ts | 21 ++ src/constants/constants.ts | 1 + src/index.ts | 2 +- src/services/clear.services.ts | 4 +- src/services/cli.settings.services.ts | 18 ++ src/services/controllers.services.ts | 22 +- src/services/deploy.services.ts | 4 +- src/services/links.services.ts | 2 +- src/services/login.services.ts | 6 +- .../upgrade.mission-control.services.ts | 4 +- .../upgrade/upgrade.orbiter.services.ts | 6 +- .../upgrade/upgrade.satellite.services.ts | 4 +- src/stores/settings.store.ts | 71 +++++++ src/types/cli.config.ts | 26 +++ src/utils/actor.utils.ts | 6 +- src/utils/config.utils.ts | 23 ++ src/utils/satellite.utils.ts | 17 +- 26 files changed, 349 insertions(+), 153 deletions(-) create mode 100644 src/configs/cli.settings.config.ts create mode 100644 src/services/cli.settings.services.ts create mode 100644 src/stores/settings.store.ts create mode 100644 src/types/cli.config.ts diff --git a/src/commands/auth.ts b/src/commands/auth.ts index eebb41f..8032979 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -8,13 +8,13 @@ import {reuseController} from '../services/controllers.services'; import {login as consoleLogin} from '../services/login.services'; export const logout = async () => { - clearCliConfig(); + await clearCliConfig(); console.log(`${green('Logged out')}`); }; export const login = async (args?: string[]) => { - const token = getToken(); + const token = await getToken(); if (isNullish(token)) { await consoleLogin(args); diff --git a/src/commands/config.ts b/src/commands/config.ts index 92bd5f1..b397070 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -19,7 +19,7 @@ export const config = async (args?: string[]) => { const {satellite: satelliteConfig} = await readJunoConfig(env); const {storage, authentication, settings} = satelliteConfig; - const satellite = satelliteParameters({satellite: satelliteConfig, env}); + const satellite = await satelliteParameters({satellite: satelliteConfig, env}); const spinner = ora(`Configuring...`).start(); diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 2e0a157..12c73fb 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -42,7 +42,7 @@ const executeDeploy = async (args?: string[]) => { await assertSatelliteMemorySize(args); }; - const satellite = satelliteParameters({satellite: satelliteConfig, env}); + const satellite = await satelliteParameters({satellite: satelliteConfig, env}); const uploadFile = async ({ filename, diff --git a/src/commands/init.ts b/src/commands/init.ts index 7541771..07b8ee8 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -5,13 +5,7 @@ import {cyan, yellow} from 'kleur'; import {unlink} from 'node:fs/promises'; import {basename} from 'node:path'; import prompts from 'prompts'; -import { - getCliOrbiters, - getCliSatellites, - getToken, - type CliOrbiterConfig, - type CliSatelliteConfig -} from '../configs/cli.config'; +import {getCliOrbiters, getCliSatellites, getToken} from '../configs/cli.config'; import { detectJunoConfigType, junoConfigExist, @@ -20,10 +14,11 @@ import { } from '../configs/juno.config'; import {promptConfigType} from '../services/init.services'; import {login as consoleLogin} from '../services/login.services'; +import type {CliOrbiterConfig, CliSatelliteConfig} from '../types/cli.config'; import {NEW_CMD_LINE, confirm, confirmAndExit} from '../utils/prompt.utils'; export const init = async (args?: string[]) => { - const token = getToken(); + const token = await getToken(); if (isNullish(token)) { const login = await confirm( @@ -79,7 +74,7 @@ const initConfig = async () => { }; const initSatelliteConfig = async (): Promise => { - const satellites = getCliSatellites(); + const satellites = await getCliSatellites(); const satellite = await (satellites?.length > 0 ? promptSatellites(satellites) @@ -93,7 +88,7 @@ const initSatelliteConfig = async (): Promise => { }; const initOrbiterConfig = async (): Promise => { - const authOrbiters = getCliOrbiters(); + const authOrbiters = await getCliOrbiters(); if (authOrbiters === undefined || authOrbiters.length === 0) { return undefined; diff --git a/src/commands/open.ts b/src/commands/open.ts index ab9ad4c..252862c 100644 --- a/src/commands/open.ts +++ b/src/commands/open.ts @@ -19,7 +19,7 @@ export const open = async (args?: string[]) => { const env = configEnv(args); const {satellite: satelliteConfig} = await readJunoConfig(env); - const satellite = satelliteParameters({satellite: satelliteConfig, env}); + const satellite = await satelliteParameters({satellite: satelliteConfig, env}); const {satelliteId} = satellite; if (hasArgs({args, options: ['-c', '--console']})) { diff --git a/src/commands/use.ts b/src/commands/use.ts index 004647e..0cdab57 100644 --- a/src/commands/use.ts +++ b/src/commands/use.ts @@ -2,24 +2,24 @@ import {hasArgs, nextArg} from '@junobuild/cli-tools'; import {green, red} from 'kleur'; import {deleteUse, getProfiles, getUse, saveUse} from '../configs/cli.config'; -export const use = (args?: string[]) => { +export const use = async (args?: string[]) => { if (hasArgs({args, options: ['-l', '--list']})) { - listProfile(); + await listProfile(); return; } - switchProfile(args); + await switchProfile(args); }; -const listProfile = () => { - const profiles = getProfiles(); +const listProfile = async () => { + const profiles = await getProfiles(); if (profiles === undefined) { console.log('No particular profiles available. Using default.'); return; } - const use = getUse(); + const use = await getUse(); console.log('Available profiles:\n'); console.log( @@ -29,7 +29,7 @@ const listProfile = () => { ); }; -const switchProfile = (args?: string[]) => { +const switchProfile = async (args?: string[]) => { const profile = nextArg({args, option: '-p'}) ?? nextArg({args, option: '--profile'}); if (profile === undefined) { @@ -38,20 +38,20 @@ const switchProfile = (args?: string[]) => { } if (profile === 'default') { - deleteUse(); + await deleteUse(); console.log(`Now using ${green('default')}.`); return; } - const profiles = getProfiles(); + const profiles = await getProfiles(); if (profiles?.[profile] === undefined) { console.log(`${red('No corresponding profile found.')}`); return; } - saveUse(profile); + await saveUse(profile); console.log(`Now using ${green(profile)}.`); }; diff --git a/src/commands/version.ts b/src/commands/version.ts index 4726a79..521d949 100644 --- a/src/commands/version.ts +++ b/src/commands/version.ts @@ -55,7 +55,7 @@ const cliVersion = async () => { }; const missionControlVersion = async () => { - const missionControl = getCliMissionControl(); + const missionControl = await getCliMissionControl(); if (isNullish(missionControl)) { console.log( @@ -68,7 +68,7 @@ const missionControlVersion = async () => { const missionControlParameters = { missionControlId: missionControl, - ...actorParameters() + ...(await actorParameters()) }; const currentVersion = await missionControlVersionLib({ @@ -93,14 +93,14 @@ const satelliteVersion = async (args?: string[]) => { const env = configEnv(args); const {satellite: satelliteConfig} = await readJunoConfig(env); - const satellite = satelliteParameters({satellite: satelliteConfig, env}); + const satellite = await satelliteParameters({satellite: satelliteConfig, env}); const {satelliteId} = satellite; const currentVersion = await satelliteVersionLib({ satellite }); - const displayHint = `satellite "${satelliteKey(satelliteId)}"`; + const displayHint = `satellite "${await satelliteKey(satelliteId)}"`; await checkSegmentVersion({ currentVersion, @@ -110,7 +110,7 @@ const satelliteVersion = async (args?: string[]) => { }; const orbitersVersion = async () => { - const orbiters = getCliOrbiters(); + const orbiters = await getCliOrbiters(); if (isNullish(orbiters) || orbiters.length === 0) { return; @@ -119,14 +119,14 @@ const orbitersVersion = async () => { const checkOrbiterVersion = async (orbiterId: string) => { const orbiterParameters = { orbiterId, - ...actorParameters() + ...(await actorParameters()) }; const currentVersion = await orbiterVersionLib({ orbiter: orbiterParameters }); - const displayHint = `orbiter "${orbiterKey(orbiterId)}"`; + const displayHint = `orbiter "${await orbiterKey(orbiterId)}"`; await checkSegmentVersion({ currentVersion, diff --git a/src/commands/whoami.ts b/src/commands/whoami.ts index 64f01cb..83d296b 100644 --- a/src/commands/whoami.ts +++ b/src/commands/whoami.ts @@ -5,7 +5,7 @@ import {getToken, getUse, isDefaultProfile} from '../configs/cli.config'; import {links} from '../services/links.services'; export const whoami = async (args?: string[]) => { - const {success} = info(); + const {success} = await info(); if (!success) { return; @@ -14,14 +14,14 @@ export const whoami = async (args?: string[]) => { await links(args); }; -const info = (): {success: boolean} => { - const profile = getUse(); +const info = async (): Promise<{success: boolean}> => { + const profile = await getUse(); if (!isDefaultProfile(profile)) { console.log(`👤 Profile: ${green(profile!)}`); } - const token = getToken(); + const token = await getToken(); if (isNullish(token)) { console.log(`No controller found.`); diff --git a/src/configs/cli.config.ts b/src/configs/cli.config.ts index cfa38e5..d47466f 100644 --- a/src/configs/cli.config.ts +++ b/src/configs/cli.config.ts @@ -1,47 +1,33 @@ import type {JsonnableEd25519KeyIdentity} from '@dfinity/identity/lib/cjs/identity/ed25519'; +import {nonNullish} from '@junobuild/utils'; // TODO: fix TypeScript declaration import of conf // @ts-expect-error -import Conf, {type Schema} from 'conf'; -import {CLI_PROJECT_NAME} from '../constants/constants'; +import type Conf from 'conf'; +import {askForPassword} from '../services/cli.settings.services'; +import {settingsStore} from '../stores/settings.store'; +import type { + CliConfig, + CliConfigData, + CliOrbiterConfig, + CliProfile, + CliSatelliteConfig +} from '../types/cli.config'; +import {loadConfig} from '../utils/config.utils'; -interface CliConfigData { - token: JsonnableEd25519KeyIdentity; - satellites: CliSatelliteConfig[]; - missionControl?: string; - orbiters?: CliOrbiterConfig[]; -} - -export interface CliSatelliteConfig { - p: string; // principal - n: string; // name -} - -export interface CliOrbiterConfig { - p: string; // principal - n?: string; // name -} - -export type CliProfile = 'default' | string; - -// Backwards compatibility. Default is save in root of the object, profile in an optional record. -interface CliConfig extends CliConfigData { - use?: CliProfile; - profiles?: Record; -} - -const schema: Schema = { - token: { - type: 'array' - }, - satellites: { - type: 'array' +// Save in https://github.com/sindresorhus/env-paths#pathsconfig +let config: Conf | undefined; + +const initConfig = async () => { + if (nonNullish(config)) { + return; } -} as const; -// Save in https://github.com/sindresorhus/env-paths#pathsconfig -const config = new Conf({projectName: CLI_PROJECT_NAME, schema}); + const encryptionKey = settingsStore.isEncryptionEnabled() ? await askForPassword() : undefined; + + config = loadConfig(encryptionKey); +}; -export const saveCliConfig = ({ +export const saveCliConfig = async ({ token, satellites, orbiters, @@ -55,9 +41,9 @@ export const saveCliConfig = ({ profile: CliProfile | null; }) => { if (!isDefaultProfile(profile)) { - const profiles = getProfiles(); + const profiles = await getProfiles(); - saveProfiles({ + await saveProfiles({ ...(profiles !== undefined ? profiles : {}), [profile!]: { token, @@ -67,50 +53,75 @@ export const saveCliConfig = ({ } }); - saveUse(profile!); + await saveUse(profile!); return; } - saveToken(token); - saveCliSatellites(satellites); + await saveToken(token); + await saveCliSatellites(satellites); if (orbiters !== null) { - saveCliOrbiters(orbiters); + await saveCliOrbiters(orbiters); } if (missionControl !== null) { - saveCliMissionControl(missionControl); + await saveCliMissionControl(missionControl); } - deleteUse(); + await deleteUse(); }; // Use / profile -export const deleteUse = () => config.delete('use'); -export const saveUse = (use: CliProfile) => config.set('use', use); -export const getUse = (): CliProfile | undefined => config.get('use'); +export const deleteUse = async () => { + await initConfig(); + + config.delete('use'); +}; +export const saveUse = async (use: CliProfile) => { + await initConfig(); + + config.set('use', use); +}; +export const getUse = async (): Promise => { + await initConfig(); + + return config.get('use'); +}; // Profile -export const saveProfiles = (profiles: Record) => +const saveProfiles = async (profiles: Record) => { + await initConfig(); + config.set('profiles', profiles); +}; -export const getProfiles = (): Record | undefined => config.get('profiles'); +export const getProfiles = async (): Promise | undefined> => { + await initConfig(); + + return config.get('profiles'); +}; export const isDefaultProfile = (use: CliProfile | undefined | null): boolean => use === null || use === undefined || use === 'default'; // Token -const saveToken = (token: JsonnableEd25519KeyIdentity) => config.set('token', token); +const saveToken = async (token: JsonnableEd25519KeyIdentity) => { + await initConfig(); + + config.set('token', token); +}; + +export const getToken = async (): Promise => { + await initConfig(); -export const getToken = (): JsonnableEd25519KeyIdentity | undefined => { - const use = getUse(); + const use = await getUse(); if (!isDefaultProfile(use)) { - return getProfiles()?.[use!]?.token; + return (await getProfiles())?.[use!]?.token; } return config.get('token'); @@ -118,20 +129,25 @@ export const getToken = (): JsonnableEd25519KeyIdentity | undefined => { // Satellites -const saveCliSatellites = (satellites: CliSatelliteConfig[]) => - config.set('satellites', satellites); +const saveCliSatellites = async (satellites: CliSatelliteConfig[]) => { + await initConfig(); + + await config.set('satellites', satellites); +}; + +export const getCliSatellites = async (): Promise => { + await initConfig(); -export const getCliSatellites = (): CliSatelliteConfig[] => { - const use = getUse(); + const use = await getUse(); if (!isDefaultProfile(use)) { - return getProfiles()?.[use!]?.satellites ?? []; + return (await getProfiles())?.[use!]?.satellites ?? []; } return config.get('satellites'); }; -export const addCliSatellite = ({ +export const addCliSatellite = async ({ satellite, profile }: { @@ -139,14 +155,14 @@ export const addCliSatellite = ({ profile: CliProfile | undefined; }) => { if (!isDefaultProfile(profile)) { - const profiles = getProfiles(); + const profiles = await getProfiles(); const currentProfile = profiles?.[profile!]; if (currentProfile === undefined) { throw new Error(`The profile must exist.`); } - saveProfiles({ + await saveProfiles({ ...(profiles !== undefined ? profiles : {}), [profile!]: { ...currentProfile, @@ -160,26 +176,34 @@ export const addCliSatellite = ({ return; } - const currentSatellites = getCliSatellites(); - saveCliSatellites([...(currentSatellites ?? []).filter(({p}) => p !== satellite.p), satellite]); + const currentSatellites = await getCliSatellites(); + await saveCliSatellites([ + ...(currentSatellites ?? []).filter(({p}) => p !== satellite.p), + satellite + ]); }; // Mission control -const saveCliMissionControl = (missionControl: string) => +const saveCliMissionControl = async (missionControl: string) => { + await initConfig(); + config.set('missionControl', missionControl); +}; -export const getCliMissionControl = (): string | undefined => { - const use = getUse(); +export const getCliMissionControl = async (): Promise => { + await initConfig(); + + const use = await getUse(); if (!isDefaultProfile(use)) { - return getProfiles()?.[use!]?.missionControl; + return (await getProfiles())?.[use!]?.missionControl; } return config.get('missionControl'); }; -export const addCliMissionControl = ({ +export const addCliMissionControl = async ({ missionControl, profile }: { @@ -187,14 +211,14 @@ export const addCliMissionControl = ({ profile: CliProfile | undefined; }) => { if (!isDefaultProfile(profile)) { - const profiles = getProfiles(); + const profiles = await getProfiles(); const currentProfile = profiles?.[profile!]; if (currentProfile === undefined) { throw new Error(`The profile must exist.`); } - saveProfiles({ + await saveProfiles({ ...(profiles !== undefined ? profiles : {}), [profile!]: { ...currentProfile, @@ -205,24 +229,30 @@ export const addCliMissionControl = ({ return; } - saveCliMissionControl(missionControl); + await saveCliMissionControl(missionControl); }; // Orbiters -const saveCliOrbiters = (orbiters: CliOrbiterConfig[]) => config.set('orbiters', orbiters); +const saveCliOrbiters = async (orbiters: CliOrbiterConfig[]) => { + await initConfig(); + + config.set('orbiters', orbiters); +}; + +export const getCliOrbiters = async (): Promise => { + await initConfig(); -export const getCliOrbiters = (): CliOrbiterConfig[] | undefined => { - const use = getUse(); + const use = await getUse(); if (!isDefaultProfile(use)) { - return getProfiles()?.[use!]?.orbiters; + return (await getProfiles())?.[use!]?.orbiters; } return config.get('orbiters'); }; -export const addCliOrbiter = ({ +export const addCliOrbiter = async ({ orbiter, profile }: { @@ -230,14 +260,14 @@ export const addCliOrbiter = ({ profile: CliProfile | undefined; }) => { if (!isDefaultProfile(profile)) { - const profiles = getProfiles(); + const profiles = await getProfiles(); const currentProfile = profiles?.[profile!]; if (currentProfile === undefined) { throw new Error(`The profile must exist.`); } - saveProfiles({ + await saveProfiles({ ...(profiles !== undefined ? profiles : {}), [profile!]: { ...currentProfile, @@ -248,10 +278,14 @@ export const addCliOrbiter = ({ return; } - const currentOrbiters = getCliOrbiters(); - saveCliOrbiters([...(currentOrbiters ?? []).filter(({p}) => p !== orbiter.p), orbiter]); + const currentOrbiters = await getCliOrbiters(); + await saveCliOrbiters([...(currentOrbiters ?? []).filter(({p}) => p !== orbiter.p), orbiter]); }; // Clear -export const clearCliConfig = () => config.clear(); +export const clearCliConfig = async () => { + await initConfig(); + + config.clear(); +}; diff --git a/src/configs/cli.settings.config.ts b/src/configs/cli.settings.config.ts new file mode 100644 index 0000000..7a4955a --- /dev/null +++ b/src/configs/cli.settings.config.ts @@ -0,0 +1,21 @@ +import {CLI_SETTINGS_NAME} from '../constants/constants'; +// TODO: fix TypeScript declaration import of conf +// @ts-expect-error +import Conf, {type Schema} from 'conf'; +import type {CliProfile} from '../types/cli.config'; + +export interface CliSettingsConfig { + encryption: boolean; +} + +const schema: Schema = { + encryption: { + type: 'boolean' + } +} as const; + +export const getSettingsConfig = (): Conf => + new Conf({projectName: CLI_SETTINGS_NAME, schema}); + +export const saveEncryption = (encryption: boolean): CliProfile | undefined => + getSettingsConfig().set('encryption', encryption); diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 91fd4e6..45555c5 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -1,5 +1,6 @@ export const AUTH_URL = `${process.env.JUNO_URL}/cli`; export const CLI_PROJECT_NAME = process.env.CLI_PROJECT_NAME; +export const CLI_SETTINGS_NAME = `${CLI_PROJECT_NAME}-cli-settings`; export const REDIRECT_URL = 'http://localhost:{port}'; export const JUNO_CONFIG_FILENAME = 'juno.config'; // .json | .js | .mjs | .cjs | .ts export const JUNO_DEV_CONFIG_FILENAME = 'juno.dev.config'; // .json | .js | .mjs | .cjs | .ts diff --git a/src/index.ts b/src/index.ts index 29c045e..6c3393a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -120,7 +120,7 @@ export const run = async () => { await whoami(args); break; case 'use': - use(args); + await use(args); break; case 'dev': await dev(args); diff --git a/src/services/clear.services.ts b/src/services/clear.services.ts index 75513ff..0cb3ee7 100644 --- a/src/services/clear.services.ts +++ b/src/services/clear.services.ts @@ -13,7 +13,7 @@ export const clear = async (args?: string[]) => { const spinner = ora('Clearing dapp assets...').start(); try { - const satellite = satelliteParameters({satellite: satelliteConfig, env}); + const satellite = await satelliteParameters({satellite: satelliteConfig, env}); // TODO: to be removed. Workaround as temporary solution of https://github.com/junobuild/juno/issues/484. const domains = await listCustomDomains({satellite}); @@ -46,7 +46,7 @@ export const clearAsset = async ({fullPath, args}: {fullPath: string; args?: str try { await deleteAsset({ collection: DAPP_COLLECTION, - satellite: satelliteParameters({satellite: satelliteConfig, env}), + satellite: await satelliteParameters({satellite: satelliteConfig, env}), fullPath: cleanFullPath(fullPath) }); } finally { diff --git a/src/services/cli.settings.services.ts b/src/services/cli.settings.services.ts new file mode 100644 index 0000000..5e85918 --- /dev/null +++ b/src/services/cli.settings.services.ts @@ -0,0 +1,18 @@ +import {assertAnswerCtrlC} from '@junobuild/cli-tools'; +import prompts from 'prompts'; + +export const askForPassword = async ( + message = 'Please provide the password for your CLI configuration.' +): Promise => { + const {encryptionKey}: {encryptionKey: string} = await prompts([ + { + type: 'password', + name: 'encryptionKey', + message + } + ]); + + assertAnswerCtrlC(encryptionKey); + + return encryptionKey; +}; diff --git a/src/services/controllers.services.ts b/src/services/controllers.services.ts index c03ce10..25099f0 100644 --- a/src/services/controllers.services.ts +++ b/src/services/controllers.services.ts @@ -30,14 +30,14 @@ export const reuseController = async (controller: Principal) => { return; } - const profile = getUse(); + const profile = await getUse(); switch (segment) { case 'orbiter': - saveOrbiter({profile, segmentId}); + await saveOrbiter({profile, segmentId}); break; case 'mission_control': - saveMissionControl({profile, segmentId}); + await saveMissionControl({profile, segmentId}); break; default: await saveSatellite({profile, segmentId}); @@ -66,7 +66,7 @@ const saveSatellite = async ({ return; } - addCliSatellite({ + await addCliSatellite({ profile, satellite: { p: segmentId, @@ -75,18 +75,24 @@ const saveSatellite = async ({ }); }; -const saveMissionControl = ({ +const saveMissionControl = async ({ profile, segmentId }: { profile: string | undefined; segmentId: string; }) => { - addCliMissionControl({profile, missionControl: segmentId}); + await addCliMissionControl({profile, missionControl: segmentId}); }; -const saveOrbiter = ({profile, segmentId}: {profile: string | undefined; segmentId: string}) => { - addCliOrbiter({ +const saveOrbiter = async ({ + profile, + segmentId +}: { + profile: string | undefined; + segmentId: string; +}) => { + await addCliOrbiter({ profile, orbiter: { p: segmentId diff --git a/src/services/deploy.services.ts b/src/services/deploy.services.ts index 69bd325..b67c153 100644 --- a/src/services/deploy.services.ts +++ b/src/services/deploy.services.ts @@ -25,7 +25,7 @@ export const assertSatelliteMemorySize = async (args?: string[]) => { return; } - const satellite = satelliteParameters({satellite: satelliteConfig, env}); + const satellite = await satelliteParameters({satellite: satelliteConfig, env}); const currentVersion = await satelliteVersion({ satellite @@ -74,7 +74,7 @@ export const listAssets = async ({ }): Promise => { const {assets, items_page, matches_pages} = await listAssetsLib({ collection: DAPP_COLLECTION, - satellite: satelliteParameters(env), + satellite: await satelliteParameters(env), filter: { order: { desc: true, diff --git a/src/services/links.services.ts b/src/services/links.services.ts index 3ef5cd0..113a210 100644 --- a/src/services/links.services.ts +++ b/src/services/links.services.ts @@ -21,7 +21,7 @@ export const links = async (args?: string[]) => { const env = configEnv(args); const {satellite: satelliteConfig} = await readJunoConfig(env); - const satellite = satelliteParameters({satellite: satelliteConfig, env}); + const satellite = await satelliteParameters({satellite: satelliteConfig, env}); const {satelliteId} = satellite; const defaultUrl = defaultSatelliteDomain(satelliteId); diff --git a/src/services/login.services.ts b/src/services/login.services.ts index b6cfa19..4a93cd7 100644 --- a/src/services/login.services.ts +++ b/src/services/login.services.ts @@ -44,7 +44,7 @@ export const login = async (args?: string[]) => { } try { - saveConfig({token, satellites, orbiters, missionControl, profile}); + await saveConfig({token, satellites, orbiters, missionControl, profile}); await respondWithFile(req, res, 200, '../templates/login/success.html'); console.log(`${green('Success!')} Logged in. ✅`); resolve(); @@ -89,7 +89,7 @@ async function respondWithFile( req.socket.destroy(); } -const saveConfig = ({ +const saveConfig = async ({ token, satellites, orbiters, @@ -102,7 +102,7 @@ const saveConfig = ({ missionControl: string | null; profile: string | null; }) => { - saveCliConfig({ + await saveCliConfig({ token, satellites: JSON.parse(decodeURIComponent(satellites ?? '[]')), orbiters: orbiters !== null ? JSON.parse(decodeURIComponent(orbiters)) : null, diff --git a/src/services/upgrade/upgrade.mission-control.services.ts b/src/services/upgrade/upgrade.mission-control.services.ts index 96a0031..4d7c47a 100644 --- a/src/services/upgrade/upgrade.mission-control.services.ts +++ b/src/services/upgrade/upgrade.mission-control.services.ts @@ -13,7 +13,7 @@ import {NEW_CMD_LINE} from '../../utils/prompt.utils'; import {selectVersion, upgradeWasmCdn, upgradeWasmLocal} from './upgrade.services'; export const upgradeMissionControl = async (args?: string[]) => { - const missionControl = getCliMissionControl(); + const missionControl = await getCliMissionControl(); if (isNullish(missionControl)) { console.log( @@ -30,7 +30,7 @@ export const upgradeMissionControl = async (args?: string[]) => { const missionControlParameters = { missionControlId: missionControl, - ...actorParameters() + ...(await actorParameters()) }; const consoleSuccess = () => { diff --git a/src/services/upgrade/upgrade.orbiter.services.ts b/src/services/upgrade/upgrade.orbiter.services.ts index e760198..966c0c3 100644 --- a/src/services/upgrade/upgrade.orbiter.services.ts +++ b/src/services/upgrade/upgrade.orbiter.services.ts @@ -13,7 +13,7 @@ import {orbiterKey} from '../../utils/satellite.utils'; import {confirmReset, selectVersion, upgradeWasmCdn, upgradeWasmLocal} from './upgrade.services'; export const upgradeOrbiters = async (args?: string[]) => { - const authOrbiters = getCliOrbiters(); + const authOrbiters = await getCliOrbiters(); if (authOrbiters === undefined || authOrbiters.length === 0) { return; @@ -24,7 +24,7 @@ export const upgradeOrbiters = async (args?: string[]) => { const orbiterParameters = { orbiterId, - ...actorParameters() + ...(await actorParameters()) }; const consoleSuccess = () => { @@ -86,7 +86,7 @@ const updateOrbiterRelease = async ({ orbiter: orbiterParameters }); - const displayHint = `orbiter "${orbiterKey(orbiterParameters.orbiterId)}"`; + const displayHint = `orbiter "${await orbiterKey(orbiterParameters.orbiterId)}"`; const version = await selectVersion({ currentVersion, assetKey: ORBITER_WASM_NAME, diff --git a/src/services/upgrade/upgrade.satellite.services.ts b/src/services/upgrade/upgrade.satellite.services.ts index 981af76..c6eb650 100644 --- a/src/services/upgrade/upgrade.satellite.services.ts +++ b/src/services/upgrade/upgrade.satellite.services.ts @@ -33,7 +33,7 @@ export const upgradeSatellite = async (args?: string[]) => { const env = configEnv(args); const {satellite: satelliteConfig} = await readJunoConfig(env); - const satellite = satelliteParameters({satellite: satelliteConfig, env}); + const satellite = await satelliteParameters({satellite: satelliteConfig, env}); const {satelliteId} = satellite; console.log( @@ -100,7 +100,7 @@ const upgradeSatelliteRelease = async ({ satellite }); - const displayHint = `satellite "${satelliteKey(satellite.satelliteId ?? '')}"`; + const displayHint = `satellite "${await satelliteKey(satellite.satelliteId ?? '')}"`; const version = await selectVersion({currentVersion, assetKey: SATELLITE_WASM_NAME, displayHint}); if (isNullish(version)) { diff --git a/src/stores/settings.store.ts b/src/stores/settings.store.ts new file mode 100644 index 0000000..795bdb0 --- /dev/null +++ b/src/stores/settings.store.ts @@ -0,0 +1,71 @@ +import {isNullish, nonNullish} from '@junobuild/utils'; +import { + type CliSettingsConfig, + getSettingsConfig, + saveEncryption +} from '../configs/cli.settings.config'; +import {confirm} from '../utils/prompt.utils'; +// TODO: fix TypeScript declaration import of conf +// @ts-expect-error +import type Conf from 'conf'; +import {yellow} from 'kleur'; +import {askForPassword} from '../services/cli.settings.services'; +import {loadConfig} from '../utils/config.utils'; + +class SettingsStore { + readonly #config: Conf; + + private constructor(readonly config: Conf) { + this.#config = config; + } + + static async init(): Promise { + const store = new SettingsStore(getSettingsConfig()); + + if (nonNullish(store.config.get('encryption'))) { + return store; + } + + const encryption = await confirm( + 'Do you want to encrypt the CLI configuration file for added security?' + ); + + saveEncryption(encryption); + + if (encryption) { + await store.migrateConfig(); + } + + return store; + } + + isEncryptionEnabled(): boolean { + return this.#config.get('encryption'); + } + + private async migrateConfig() { + try { + const config = loadConfig(undefined); + + // We load a config object that contains no entries, therefore there is no configuration to migrate. + if (isNullish(config.store) || config.size === 0) { + return; + } + + const pwd = await askForPassword( + 'Please provide a password to encrypt your CLI configuration file' + ); + + // Save with encryption. + const configEncoded = loadConfig(pwd); + configEncoded.store = config.store; + } catch (err: unknown) { + console.log( + `${yellow('Your current configuration cannot be encrypted. Maybe it is already encrypted?')}` + ); + console.log(err); + } + } +} + +export const settingsStore: SettingsStore = await SettingsStore.init(); diff --git a/src/types/cli.config.ts b/src/types/cli.config.ts new file mode 100644 index 0000000..acc5931 --- /dev/null +++ b/src/types/cli.config.ts @@ -0,0 +1,26 @@ +import type {JsonnableEd25519KeyIdentity} from '@dfinity/identity/lib/cjs/identity/ed25519'; + +export interface CliConfigData { + token: JsonnableEd25519KeyIdentity; + satellites: CliSatelliteConfig[]; + missionControl?: string; + orbiters?: CliOrbiterConfig[]; +} + +export interface CliSatelliteConfig { + p: string; // principal + n: string; // name +} + +export interface CliOrbiterConfig { + p: string; // principal + n?: string; // name +} + +export type CliProfile = 'default' | string; + +// Backwards compatibility. Default is save in root of the object, profile in an optional record. +export interface CliConfig extends CliConfigData { + use?: CliProfile; + profiles?: Record; +} diff --git a/src/utils/actor.utils.ts b/src/utils/actor.utils.ts index 2c25f1d..317b319 100644 --- a/src/utils/actor.utils.ts +++ b/src/utils/actor.utils.ts @@ -7,8 +7,8 @@ import {getToken} from '../configs/cli.config'; import {REVOKED_CONTROLLERS} from '../constants/constants'; import {getProcessToken} from './process.utils'; -export const actorParameters = (): ActorParameters => { - const token = getProcessToken() ?? getToken(); +export const actorParameters = async (): Promise => { + const token = getProcessToken() ?? (await getToken()); if (isNullish(token)) { console.log(`${red('No controller found.')} Are you logged in?`); @@ -29,7 +29,7 @@ export const actorParameters = (): ActorParameters => { }; export const initAgent = async (): Promise => { - const {identity, container, fetch} = actorParameters(); + const {identity, container, fetch} = await actorParameters(); const localActor = nonNullish(container) && container !== false; diff --git a/src/utils/config.utils.ts b/src/utils/config.utils.ts index 26b3517..1b581ea 100644 --- a/src/utils/config.utils.ts +++ b/src/utils/config.utils.ts @@ -1,5 +1,11 @@ import {nextArg} from '@junobuild/cli-tools'; import type {JunoConfigEnv} from '@junobuild/config'; +import {nonNullish} from '@junobuild/utils'; +import {CLI_PROJECT_NAME} from '../constants/constants'; +// TODO: fix TypeScript declaration import of conf +// @ts-expect-error +import Conf, {type Schema} from 'conf'; +import type {CliConfig} from '../types/cli.config'; export const configEnv = (args?: string[]): JunoConfigEnv => { const mode = nextArg({args, option: '-m'}) ?? nextArg({args, option: '--mode'}); @@ -7,3 +13,20 @@ export const configEnv = (args?: string[]): JunoConfigEnv => { mode: mode ?? 'production' }; }; + +export const loadConfig = (encryptionKey: string | undefined): Conf => { + const schema: Schema = { + token: { + type: 'array' + }, + satellites: { + type: 'array' + } + } as const; + + return new Conf({ + projectName: CLI_PROJECT_NAME, + schema, + ...(nonNullish(encryptionKey) && {encryptionKey}) + }); +}; diff --git a/src/utils/satellite.utils.ts b/src/utils/satellite.utils.ts index 8594d90..8919c3d 100644 --- a/src/utils/satellite.utils.ts +++ b/src/utils/satellite.utils.ts @@ -5,11 +5,12 @@ import {getCliOrbiters, getCliSatellites} from '../configs/cli.config'; import type {SatelliteConfigEnv} from '../types/config'; import {actorParameters} from './actor.utils'; -export const satelliteParameters = ({ +export const satelliteParameters = async ({ satellite: {satelliteId: deprecatedSatelliteId, id, ids}, env: {mode} -}: SatelliteConfigEnv): Omit & - Required> => { +}: SatelliteConfigEnv): Promise< + Omit & Required> +> => { const satelliteId = ids?.[mode] ?? id ?? deprecatedSatelliteId; if (isNullish(satelliteId)) { @@ -19,7 +20,7 @@ export const satelliteParameters = ({ return { satelliteId, - ...actorParameters() + ...(await actorParameters()) }; }; @@ -27,14 +28,14 @@ export const satelliteParameters = ({ * For display purpose, use either the name or id. Most probably we should find a name but for simplicity reason we fallback to Id. * @param satelliteId name or id */ -export const satelliteKey = (satelliteId: string): string => { - const satellites = getCliSatellites(); +export const satelliteKey = async (satelliteId: string): Promise => { + const satellites = await getCliSatellites(); const satellite = satellites.find(({p}) => p === satelliteId); return satellite?.n ?? satelliteId; }; -export const orbiterKey = (orbiterId: string): string => { - const orbiters = getCliOrbiters(); +export const orbiterKey = async (orbiterId: string): Promise => { + const orbiters = await getCliOrbiters(); const orbiter = orbiters?.find(({p}) => p === orbiterId); return orbiter?.n !== undefined && orbiter?.n !== '' ? orbiter.n : orbiterId; };