Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions src/commands/auth.ts
Original file line number Diff line number Diff line change
@@ -1,11 +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 {login as consoleLogin} from '../services/auth/login.services';
import {DEV} from '../env';
import {loginEmulatorOnly} from '../services/auth/login.emulator.services';
import {login as loginServices} from '../services/auth/login.services';
import {reuseController} from '../services/controllers.services';
import {confirmAndExit} from '../utils/prompt.utils';

export const logout = async () => {
await clearCliConfig();
Expand All @@ -14,10 +17,19 @@ export const logout = async () => {
};

export const login = async (args?: string[]) => {
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;
}

Expand All @@ -37,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();
};
1 change: 1 addition & 0 deletions src/help/login.help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;

Expand Down
77 changes: 77 additions & 0 deletions src/services/auth/login.emulator.services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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();

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);
const {satelliteId} = assertConfigAndReadSatelliteId({satellite: satelliteConfig, env: ENV});

const parsedResult = await readEmulatorConfig();

if (!parsedResult.success) {
return {status: 'validation-error'};
}

const {principal, token} = generateToken();

const {result} = await dispatchRequest({
config: parsedResult.config,
request: `satellite/controller/?id=${principal}&satelliteId=${satelliteId}${notEmptyString(ENV.profile) ? `&profile=${encodeURIComponent(ENV.profile)}` : ''}`,
timeout: 10000
});

if (result !== 'ok') {
return {status: 'error'};
}

await saveCliConfig({
token,
satellites: [{p: satelliteId, n: ''}],
orbiters: null,
missionControl: null
});

return {status: 'success', principal};
};
7 changes: 2 additions & 5 deletions src/services/auth/login.services.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand All @@ -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)}`);

Expand Down
10 changes: 6 additions & 4 deletions src/services/emulator/emulator.admin.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import type {CliEmulatorConfig} from '../../types/emulator';

export const dispatchRequest = async ({
config: emulatorConfig,
adminRequest
request,
timeout = 5000
}: {
config: CliEmulatorConfig;
adminRequest: string;
request: string;
timeout?: number;
}): Promise<
| {result: 'ok'; response: Response}
| {result: 'not_ok'; response: Response}
Expand All @@ -20,8 +22,8 @@ export const dispatchRequest = async ({
const adminPort = config[emulatorType]?.ports?.admin ?? EMULATOR_SKYLAB.ports.admin;

try {
const response = await fetch(`http://localhost:${adminPort}/admin/${adminRequest}`, {
signal: AbortSignal.timeout(5000)
const response = await fetch(`http://localhost:${adminPort}/${request}`, {
signal: AbortSignal.timeout(timeout)
});

if (!response.ok) {
Expand Down
2 changes: 1 addition & 1 deletion src/services/functions/build/touch.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions src/utils/auth.utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import {Ed25519KeyIdentity} from '@dfinity/identity';
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';
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,
Expand Down
17 changes: 13 additions & 4 deletions src/utils/satellite.utils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -29,10 +30,10 @@ export const assertConfigAndLoadSatelliteContext = async (): Promise<{
return {satellite, satelliteConfig};
};

const satelliteParameters = async ({
export const assertConfigAndReadSatelliteId = ({
satellite,
env: {mode}
}: SatelliteConfigEnv): Promise<SatelliteParametersWithId> => {
}: SatelliteConfigEnv): {satelliteId: PrincipalText} => {
const {id, ids} = satellite;

// Originally, the config used `satelliteId`, but we later migrated to `id` and `ids`.
Expand All @@ -41,7 +42,7 @@ const satelliteParameters = async ({
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;
Expand All @@ -51,6 +52,14 @@ const satelliteParameters = async ({
process.exit(1);
}

return {satelliteId};
};

const satelliteParameters = async (
params: SatelliteConfigEnv
): Promise<SatelliteParametersWithId> => {
const {satelliteId} = assertConfigAndReadSatelliteId(params);

return {
satelliteId,
...(await actorParameters())
Expand Down