From a4acdc2122e26ef0f598dd7f71d4c82e4e60145c Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 1 Aug 2025 13:35:50 +0200 Subject: [PATCH 1/5] feat: headless development juno login --- src/commands/auth.ts | 8 ++++ src/services/auth/login.emulator.services.ts | 43 ++++++++++++++++++++ src/services/auth/login.services.ts | 7 +--- src/utils/auth.utils.ts | 11 +++++ 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/services/auth/login.emulator.services.ts diff --git a/src/commands/auth.ts b/src/commands/auth.ts index caa8e27a..720fad6b 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -4,8 +4,11 @@ import {assertAnswerCtrlC} from '@junobuild/cli-tools'; import {green} from 'kleur'; import prompts from 'prompts'; import {clearCliConfig, getToken} from '../configs/cli.config'; +import {DEV} from '../env'; +import {loginEmulatorOnly} from '../services/auth/login.emulator.services'; import {login as consoleLogin} from '../services/auth/login.services'; import {reuseController} from '../services/controllers.services'; +import {isHeadless} from '../utils/process.utils'; export const logout = async () => { await clearCliConfig(); @@ -16,6 +19,11 @@ export const logout = async () => { export const login = async (args?: string[]) => { const token = await getToken(); + if (isNullish(token) && isHeadless() && DEV) { + await loginEmulatorOnly(); + return; + } + if (isNullish(token)) { await consoleLogin(args); return; diff --git a/src/services/auth/login.emulator.services.ts b/src/services/auth/login.emulator.services.ts new file mode 100644 index 00000000..3985c4d1 --- /dev/null +++ b/src/services/auth/login.emulator.services.ts @@ -0,0 +1,43 @@ +import {readEmulatorConfig} from '../../configs/emulator.config'; +import {generateToken} from '../../utils/auth.utils'; +import {dispatchRequest} from '../emulator/emulator.admin.services'; + +import {green, red} from 'kleur'; +import {saveCliConfig} from '../../configs/cli.config'; +import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; + +export const loginEmulatorOnly = async (args?: string[]) => { + const {satellite} = await assertConfigAndLoadSatelliteContext(); + const {satelliteId} = satellite; + + const parsedResult = await readEmulatorConfig(); + + if (!parsedResult.success) { + return; + } + + const {principal, token} = generateToken(); + + const {result} = await dispatchRequest({ + config: parsedResult.config, + adminRequest: `satellite/controller/?id=${principal}` + }); + + if (result !== 'ok') { + console.log( + red( + 'Unable to register your terminal as an access key for the Satellite running in the emulator.' + ) + ); + return; + } + + await saveCliConfig({ + token, + satellites: [{p: satelliteId, n: ""}], + orbiters: null, + missionControl: null + }); + + console.log(`\nšŸ”“ Your terminal is authenticated with admin access as: ${green(principal)}`); +}; diff --git a/src/services/auth/login.services.ts b/src/services/auth/login.services.ts index 5b9a8ebb..84770e8f 100644 --- a/src/services/auth/login.services.ts +++ b/src/services/auth/login.services.ts @@ -1,4 +1,3 @@ -import {Ed25519KeyIdentity} from '@dfinity/identity'; import type {JsonnableEd25519KeyIdentity} from '@dfinity/identity/lib/cjs/identity/ed25519'; import {nextArg} from '@junobuild/cli-tools'; import {bold, green, underline} from 'kleur'; @@ -10,7 +9,7 @@ import path, {dirname} from 'node:path'; import {fileURLToPath} from 'node:url'; import util from 'node:util'; import {saveCliConfig} from '../../configs/cli.config'; -import {authUrl, requestUrl} from '../../utils/auth.utils'; +import {authUrl, generateToken, requestUrl} from '../../utils/auth.utils'; import {openUrl} from '../../utils/open.utils'; import {getPort} from '../../utils/port.utils'; @@ -21,9 +20,7 @@ export const login = async (args?: string[]) => { const port = await getPort(); const nonce = randomBytes(16).toString('hex'); - const key = Ed25519KeyIdentity.generate(); - const principal = key.getPrincipal().toText(); - const token = key.toJSON(); + const {principal, token} = generateToken(); console.log(`\nšŸ”“ Your terminal will authenticate with admin access as: ${green(principal)}`); diff --git a/src/utils/auth.utils.ts b/src/utils/auth.utils.ts index bc651091..5eca196b 100644 --- a/src/utils/auth.utils.ts +++ b/src/utils/auth.utils.ts @@ -1,7 +1,18 @@ +import {Ed25519KeyIdentity} from '@dfinity/identity'; +import {JsonnableEd25519KeyIdentity} from '@dfinity/identity/lib/cjs/identity/ed25519'; import {nonNullish} from '@dfinity/utils'; +import type {PrincipalText} from '@dfinity/zod-schemas'; import {REDIRECT_URL} from '../constants/constants'; import {ENV} from '../env'; +export const generateToken = (): {principal: PrincipalText; token: JsonnableEd25519KeyIdentity} => { + const key = Ed25519KeyIdentity.generate(); + const principal = key.getPrincipal().toText(); + const token = key.toJSON(); + + return {principal, token}; +}; + export const authUrl = ({ port, nonce, From 5152e7d07eeaae4dce3bee1c2c20a048d8beee06 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 1 Aug 2025 14:00:39 +0200 Subject: [PATCH 2/5] feat: read only satellite id --- src/services/auth/login.emulator.services.ts | 14 +++++++++----- src/utils/satellite.utils.ts | 12 ++++++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/services/auth/login.emulator.services.ts b/src/services/auth/login.emulator.services.ts index 3985c4d1..3a938434 100644 --- a/src/services/auth/login.emulator.services.ts +++ b/src/services/auth/login.emulator.services.ts @@ -4,11 +4,15 @@ import {dispatchRequest} from '../emulator/emulator.admin.services'; import {green, red} from 'kleur'; import {saveCliConfig} from '../../configs/cli.config'; -import {assertConfigAndLoadSatelliteContext} from '../../utils/satellite.utils'; +import {readJunoConfig} from '../../configs/juno.config'; +import {ENV} from '../../env'; +import {assertConfigAndReadSatelliteId} from '../../utils/satellite.utils'; -export const loginEmulatorOnly = async (args?: string[]) => { - const {satellite} = await assertConfigAndLoadSatelliteContext(); - const {satelliteId} = satellite; +export const loginEmulatorOnly = async () => { + // We read directly the Juno config because we cannot load an actor at this point as we are login in. + // i.e. we cannot use assertConfigAndLoadSatelliteContext + const {satellite: satelliteConfig} = await readJunoConfig(ENV); + const {satelliteId} = assertConfigAndReadSatelliteId({satellite: satelliteConfig, env: ENV}); const parsedResult = await readEmulatorConfig(); @@ -34,7 +38,7 @@ export const loginEmulatorOnly = async (args?: string[]) => { await saveCliConfig({ token, - satellites: [{p: satelliteId, n: ""}], + satellites: [{p: satelliteId, n: ''}], orbiters: null, missionControl: null }); diff --git a/src/utils/satellite.utils.ts b/src/utils/satellite.utils.ts index ba8b259d..08d6d55e 100644 --- a/src/utils/satellite.utils.ts +++ b/src/utils/satellite.utils.ts @@ -29,10 +29,10 @@ export const assertConfigAndLoadSatelliteContext = async (): Promise<{ return {satellite, satelliteConfig}; }; -const satelliteParameters = async ({ +export const assertConfigAndReadSatelliteId = ({ satellite, env: {mode} -}: SatelliteConfigEnv): Promise => { +}: SatelliteConfigEnv): {satelliteId: string} => { const {id, ids} = satellite; // Originally, the config used `satelliteId`, but we later migrated to `id` and `ids`. @@ -51,6 +51,14 @@ const satelliteParameters = async ({ process.exit(1); } + return {satelliteId}; +}; + +const satelliteParameters = async ( + params: SatelliteConfigEnv +): Promise => { + const {satelliteId} = assertConfigAndReadSatelliteId(params); + return { satelliteId, ...(await actorParameters()) From a21ff7058eee8560ceed830ccacf70edf1809ac2 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 1 Aug 2025 14:36:43 +0200 Subject: [PATCH 3/5] feat: use --emulator args --- src/commands/auth.ts | 47 +++++++++++++++---- src/help/login.help.ts | 1 + src/services/auth/login.emulator.services.ts | 2 +- .../emulator/emulator.admin.services.ts | 6 +-- .../functions/build/touch.services.ts | 2 +- src/utils/auth.utils.ts | 2 +- src/utils/satellite.utils.ts | 7 +-- 7 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/commands/auth.ts b/src/commands/auth.ts index 720fad6b..d1c94acc 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -1,14 +1,14 @@ import {Ed25519KeyIdentity} from '@dfinity/identity'; import {isNullish} from '@dfinity/utils'; -import {assertAnswerCtrlC} from '@junobuild/cli-tools'; -import {green} from 'kleur'; +import {assertAnswerCtrlC, hasArgs} from '@junobuild/cli-tools'; +import {green, red} from 'kleur'; import prompts from 'prompts'; import {clearCliConfig, getToken} from '../configs/cli.config'; import {DEV} from '../env'; import {loginEmulatorOnly} from '../services/auth/login.emulator.services'; -import {login as consoleLogin} from '../services/auth/login.services'; +import {login as loginServices} from '../services/auth/login.services'; import {reuseController} from '../services/controllers.services'; -import {isHeadless} from '../utils/process.utils'; +import {confirmAndExit} from '../utils/prompt.utils'; export const logout = async () => { await clearCliConfig(); @@ -17,15 +17,19 @@ export const logout = async () => { }; export const login = async (args?: string[]) => { - const token = await getToken(); - - if (isNullish(token) && isHeadless() && DEV) { - await loginEmulatorOnly(); + if (hasArgs({args, options: ['-e', '--emulator']})) { + await emulatorLogin(); return; } + await consoleLogin(args); +}; + +const consoleLogin = async (args?: string[]) => { + const token = await getToken(); + if (isNullish(token)) { - await consoleLogin(args); + await loginServices(args); return; } @@ -45,9 +49,32 @@ export const login = async (args?: string[]) => { assertAnswerCtrlC(action); if (action === 'login') { - await consoleLogin(args); + await loginServices(args); return; } await reuseController(identity.getPrincipal()); }; + +const emulatorLogin = async () => { + if (!DEV) { + console.log(red('The login option --emulator is only supported in development mode.')); + return; + } + + const token = await getToken(); + + if (isNullish(token)) { + await loginEmulatorOnly(); + return; + } + + const identity = Ed25519KeyIdentity.fromParsedJson(token); + console.log(`šŸ” Your terminal already has access: ${green(identity.getPrincipal().toText())}\n`); + + await confirmAndExit( + 'Would you like to overwrite the saved development authentication on this device' + ); + + await loginEmulatorOnly(); +}; diff --git a/src/help/login.help.ts b/src/help/login.help.ts index b91ab485..3c64c7fa 100644 --- a/src/help/login.help.ts +++ b/src/help/login.help.ts @@ -7,6 +7,7 @@ const usage = `Usage: ${green('juno')} ${cyan('login')} ${yellow('[options]')} Options: ${yellow('-b, --browser')} A particular browser to open. supported: chrome|firefox|edge. + ${yellow('-e, --emulator')} Skips the Console UI and logs in your terminal with the emulator (āš ļø local development only). ${OPTIONS_CONFIG} ${OPTION_HELP}`; diff --git a/src/services/auth/login.emulator.services.ts b/src/services/auth/login.emulator.services.ts index 3a938434..9b70106f 100644 --- a/src/services/auth/login.emulator.services.ts +++ b/src/services/auth/login.emulator.services.ts @@ -24,7 +24,7 @@ export const loginEmulatorOnly = async () => { const {result} = await dispatchRequest({ config: parsedResult.config, - adminRequest: `satellite/controller/?id=${principal}` + request: `satellite/controller/?id=${principal}&satelliteId=${satelliteId}` }); if (result !== 'ok') { diff --git a/src/services/emulator/emulator.admin.services.ts b/src/services/emulator/emulator.admin.services.ts index 056a12cc..2d4d1921 100644 --- a/src/services/emulator/emulator.admin.services.ts +++ b/src/services/emulator/emulator.admin.services.ts @@ -3,10 +3,10 @@ import type {CliEmulatorConfig} from '../../types/emulator'; export const dispatchRequest = async ({ config: emulatorConfig, - adminRequest + request }: { config: CliEmulatorConfig; - adminRequest: string; + request: string; }): Promise< | {result: 'ok'; response: Response} | {result: 'not_ok'; response: Response} @@ -20,7 +20,7 @@ export const dispatchRequest = async ({ const adminPort = config[emulatorType]?.ports?.admin ?? EMULATOR_SKYLAB.ports.admin; try { - const response = await fetch(`http://localhost:${adminPort}/admin/${adminRequest}`, { + const response = await fetch(`http://localhost:${adminPort}/${request}`, { signal: AbortSignal.timeout(5000) }); diff --git a/src/services/functions/build/touch.services.ts b/src/services/functions/build/touch.services.ts index 096648e0..6add5c9d 100644 --- a/src/services/functions/build/touch.services.ts +++ b/src/services/functions/build/touch.services.ts @@ -42,7 +42,7 @@ const dispatchTouch = async ({filename}: {filename: string}) => { const {result} = await dispatchRequest({ config: parsedResult.config, - adminRequest: `touch?file=${encodeURIComponent(filename)}` + request: `admin/touch?file=${encodeURIComponent(filename)}` }); // We silence the error (result === error). Maybe the emulator is not running on purpose. diff --git a/src/utils/auth.utils.ts b/src/utils/auth.utils.ts index 5eca196b..88c82fd0 100644 --- a/src/utils/auth.utils.ts +++ b/src/utils/auth.utils.ts @@ -1,5 +1,5 @@ import {Ed25519KeyIdentity} from '@dfinity/identity'; -import {JsonnableEd25519KeyIdentity} from '@dfinity/identity/lib/cjs/identity/ed25519'; +import type {JsonnableEd25519KeyIdentity} from '@dfinity/identity/lib/cjs/identity/ed25519'; import {nonNullish} from '@dfinity/utils'; import type {PrincipalText} from '@dfinity/zod-schemas'; import {REDIRECT_URL} from '../constants/constants'; diff --git a/src/utils/satellite.utils.ts b/src/utils/satellite.utils.ts index 08d6d55e..55db136a 100644 --- a/src/utils/satellite.utils.ts +++ b/src/utils/satellite.utils.ts @@ -1,5 +1,6 @@ import {assertNonNullish, isNullish} from '@dfinity/utils'; -import {type SatelliteConfig} from '@junobuild/config'; +import type {PrincipalText} from '@dfinity/zod-schemas'; +import type {SatelliteConfig} from '@junobuild/config'; import {red} from 'kleur'; import {actorParameters} from '../api/actor.api'; import {getCliOrbiters, getCliSatellites} from '../configs/cli.config'; @@ -32,7 +33,7 @@ export const assertConfigAndLoadSatelliteContext = async (): Promise<{ export const assertConfigAndReadSatelliteId = ({ satellite, env: {mode} -}: SatelliteConfigEnv): {satelliteId: string} => { +}: SatelliteConfigEnv): {satelliteId: PrincipalText} => { const {id, ids} = satellite; // Originally, the config used `satelliteId`, but we later migrated to `id` and `ids`. @@ -41,7 +42,7 @@ export const assertConfigAndReadSatelliteId = ({ const deprecatedSatelliteId = 'satelliteId' in satellite ? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - (satellite as unknown as {satelliteId: string}).satelliteId + (satellite as unknown as {satelliteId: PrincipalText}).satelliteId : undefined; const satelliteId = ids?.[mode] ?? id ?? deprecatedSatelliteId; From bf3bc69023eb5fd8e723904648f615c1de368ae4 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 1 Aug 2025 15:21:17 +0200 Subject: [PATCH 4/5] feat: pass profile and timeout and error display --- src/services/auth/login.emulator.services.ts | 50 +++++++++++++++---- .../emulator/emulator.admin.services.ts | 6 ++- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/services/auth/login.emulator.services.ts b/src/services/auth/login.emulator.services.ts index 9b70106f..c5f7e279 100644 --- a/src/services/auth/login.emulator.services.ts +++ b/src/services/auth/login.emulator.services.ts @@ -1,14 +1,48 @@ import {readEmulatorConfig} from '../../configs/emulator.config'; import {generateToken} from '../../utils/auth.utils'; import {dispatchRequest} from '../emulator/emulator.admin.services'; - +import {notEmptyString} from '@dfinity/utils'; +import {type PrincipalText} from '@dfinity/zod-schemas'; import {green, red} from 'kleur'; +import ora from 'ora'; import {saveCliConfig} from '../../configs/cli.config'; import {readJunoConfig} from '../../configs/juno.config'; import {ENV} from '../../env'; import {assertConfigAndReadSatelliteId} from '../../utils/satellite.utils'; export const loginEmulatorOnly = async () => { + const spinner = ora('Granting terminal access...').start(); + + try { + const result = await loginEmulator(); + + spinner.stop(); + + if (result.status === 'validation-error') { + return; + } + + if (result.status === 'error') { + console.log( + red( + `\nUnable to register your terminal as an access key for the Satellite running in the emulator.` + ) + ); + return; + } + + console.log( + `\nšŸ”“ Your terminal is authenticated with admin access as: ${green(result.principal)}` + ); + } catch (err: unknown) { + spinner.stop(); + throw err; + } +}; + +const loginEmulator = async (): Promise< + {status: 'success'; principal: PrincipalText} | {status: 'error'} | {status: 'validation-error'} +> => { // We read directly the Juno config because we cannot load an actor at this point as we are login in. // i.e. we cannot use assertConfigAndLoadSatelliteContext const {satellite: satelliteConfig} = await readJunoConfig(ENV); @@ -17,23 +51,19 @@ export const loginEmulatorOnly = async () => { const parsedResult = await readEmulatorConfig(); if (!parsedResult.success) { - return; + return {status: 'validation-error'}; } const {principal, token} = generateToken(); const {result} = await dispatchRequest({ config: parsedResult.config, - request: `satellite/controller/?id=${principal}&satelliteId=${satelliteId}` + request: `satellite/controller/?id=${principal}&satelliteId=${satelliteId}${notEmptyString(ENV.profile) ? `&profile=${encodeURIComponent(ENV.profile)}` : ''}`, + timeout: 10000 }); if (result !== 'ok') { - console.log( - red( - 'Unable to register your terminal as an access key for the Satellite running in the emulator.' - ) - ); - return; + return {status: 'error'}; } await saveCliConfig({ @@ -43,5 +73,5 @@ export const loginEmulatorOnly = async () => { missionControl: null }); - console.log(`\nšŸ”“ Your terminal is authenticated with admin access as: ${green(principal)}`); + return {status: 'success', principal}; }; diff --git a/src/services/emulator/emulator.admin.services.ts b/src/services/emulator/emulator.admin.services.ts index 2d4d1921..9b347f41 100644 --- a/src/services/emulator/emulator.admin.services.ts +++ b/src/services/emulator/emulator.admin.services.ts @@ -3,10 +3,12 @@ import type {CliEmulatorConfig} from '../../types/emulator'; export const dispatchRequest = async ({ config: emulatorConfig, - request + request, + timeout = 5000 }: { config: CliEmulatorConfig; request: string; + timeout?: number; }): Promise< | {result: 'ok'; response: Response} | {result: 'not_ok'; response: Response} @@ -21,7 +23,7 @@ export const dispatchRequest = async ({ try { const response = await fetch(`http://localhost:${adminPort}/${request}`, { - signal: AbortSignal.timeout(5000) + signal: AbortSignal.timeout(timeout) }); if (!response.ok) { From 934232c3276fd880f7183d27a0ea0ebaf618fd05 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Fri, 1 Aug 2025 15:24:10 +0200 Subject: [PATCH 5/5] chore: fmt --- src/services/auth/login.emulator.services.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/auth/login.emulator.services.ts b/src/services/auth/login.emulator.services.ts index c5f7e279..3bac82db 100644 --- a/src/services/auth/login.emulator.services.ts +++ b/src/services/auth/login.emulator.services.ts @@ -1,14 +1,14 @@ -import {readEmulatorConfig} from '../../configs/emulator.config'; -import {generateToken} from '../../utils/auth.utils'; -import {dispatchRequest} from '../emulator/emulator.admin.services'; import {notEmptyString} from '@dfinity/utils'; import {type PrincipalText} from '@dfinity/zod-schemas'; import {green, red} from 'kleur'; import ora from 'ora'; import {saveCliConfig} from '../../configs/cli.config'; +import {readEmulatorConfig} from '../../configs/emulator.config'; import {readJunoConfig} from '../../configs/juno.config'; import {ENV} from '../../env'; +import {generateToken} from '../../utils/auth.utils'; import {assertConfigAndReadSatelliteId} from '../../utils/satellite.utils'; +import {dispatchRequest} from '../emulator/emulator.admin.services'; export const loginEmulatorOnly = async () => { const spinner = ora('Granting terminal access...').start();