Skip to content
Open
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
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,42 @@ USAGE
<!-- usagestop -->
## Commands
<!-- commands -->
* [`hd auth login`](#hd-auth-login)
* [`hd auth logout`](#hd-auth-logout)
* [`hd help [COMMAND]`](#hd-help-command)
* [`hd report committers`](#hd-report-committers)
* [`hd scan eol`](#hd-scan-eol)
* [`hd tracker init`](#hd-tracker-init)
* [`hd tracker run`](#hd-tracker-run)
* [`hd update [CHANNEL]`](#hd-update-channel)
* **NOTE:** Only applies to [binary installation method](#binary-installation). NPM users should use [`npm install`](#global-npm-installation) to update to the latest version.

## `hd auth login`

OAuth CLI login

```
USAGE
$ hd auth login

DESCRIPTION
OAuth CLI login
```

_See code: [src/commands/auth/login.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.14/src/commands/auth/login.ts)_

## `hd auth logout`

Logs out of HeroDevs OAuth and clears stored tokens

```
USAGE
$ hd auth logout

DESCRIPTION
Logs out of HeroDevs OAuth and clears stored tokens
```

_See code: [src/commands/auth/logout.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.14/src/commands/auth/logout.ts)_

## `hd help [COMMAND]`

Expand Down
6 changes: 3 additions & 3 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import http from 'node:http';
import { createInterface } from 'node:readline';
import { URL } from 'node:url';
import { Command } from '@oclif/core';
import { CLIENT_ID, REALM_URL } from '../../config/auth.config.js';
import { persistTokenResponse } from '../../service/auth.svc.ts';
import { getClientId, getRealmUrl } from '../../service/auth-config.svc.ts';
import type { TokenResponse } from '../../types/auth.ts';
import { openInBrowser } from '../../utils/open-in-browser.ts';

Expand All @@ -14,8 +14,8 @@ export default class AuthLogin extends Command {
private server?: http.Server;
private readonly port = parseInt(process.env.OAUTH_CALLBACK_PORT || '4000', 10);
private readonly redirectUri = process.env.OAUTH_CALLBACK_REDIRECT || `http://localhost:${this.port}/oauth2/callback`;
private readonly realmUrl = getRealmUrl();
private readonly clientId = getClientId();
private readonly realmUrl = REALM_URL;
private readonly clientId = CLIENT_ID;

async run() {
if (typeof (this.config as { runHook?: unknown }).runHook === 'function') {
Expand Down
11 changes: 11 additions & 0 deletions src/config/auth.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const DEFAULT_REALM_URL = 'https://idp.prod.apps.herodevs.io/realms/universe/protocol/openid-connect';
const DEFAULT_CLIENT_ID = 'default-public';
const DEFAULT_SERVICE_NAME = '@herodevs/cli';
const DEFAULT_ACCESS_KEY = 'access-token';
const DEFAULT_REFRESH_KEY = 'refresh-token';

export const REALM_URL = process.env.OAUTH_CONNECT_URL || DEFAULT_REALM_URL;
export const CLIENT_ID = process.env.OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID;
export const SERVICE_NAME = process.env.HD_AUTH_SERVICE_NAME || DEFAULT_SERVICE_NAME;
export const ACCESS_KEY = process.env.HD_AUTH_ACCESS_KEY || DEFAULT_ACCESS_KEY;
export const REFRESH_KEY = process.env.HD_AUTH_REFRESH_KEY || DEFAULT_REFRESH_KEY;
25 changes: 0 additions & 25 deletions src/service/auth-config.svc.ts

This file was deleted.

10 changes: 5 additions & 5 deletions src/service/auth-refresh.svc.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { CLIENT_ID, REALM_URL } from '../config/auth.config.js';
import type { TokenResponse } from '../types/auth.ts';
import { getClientId, getRealmUrl } from './auth-config.svc.ts';

interface AuthOptions {
clientId?: string;
realmUrl?: string;
}

export async function refreshTokens(refreshToken: string, options: AuthOptions = {}): Promise<TokenResponse> {
const clientId = options.clientId ?? getClientId();
const realmUrl = options.realmUrl ?? getRealmUrl();
const clientId = options.clientId ?? CLIENT_ID;
const realmUrl = options.realmUrl ?? REALM_URL;
const tokenUrl = `${realmUrl}/token`;

const body = new URLSearchParams({
Expand Down Expand Up @@ -36,8 +36,8 @@ export async function logoutFromProvider(refreshToken: string | undefined, optio
return;
}

const clientId = options.clientId ?? getClientId();
const realmUrl = options.realmUrl ?? getRealmUrl();
const clientId = options.clientId ?? CLIENT_ID;
const realmUrl = options.realmUrl ?? REALM_URL;
const logoutUrl = `${realmUrl}/logout`;

const body = new URLSearchParams({
Expand Down
28 changes: 8 additions & 20 deletions src/service/auth-token.svc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AsyncEntry } from '@napi-rs/keyring';
import { getAccessTokenKey, getRefreshTokenKey, getTokenServiceName } from './auth-config.svc.ts';
import { ACCESS_KEY, REFRESH_KEY, SERVICE_NAME } from '../config/auth.config.js';

export interface StoredTokens {
accessToken?: string;
Expand All @@ -9,26 +9,18 @@ export interface StoredTokens {
const TOKEN_SKEW_SECONDS = 30;

export async function saveTokens(tokens: { accessToken: string; refreshToken?: string }) {
const service = getTokenServiceName();
const accessKey = getAccessTokenKey();
const refreshKey = getRefreshTokenKey();

const accessTokenSet = new AsyncEntry(service, accessKey).setPassword(tokens.accessToken);
const accessTokenSet = new AsyncEntry(SERVICE_NAME, ACCESS_KEY).setPassword(tokens.accessToken);
const refreshTokenSet = tokens.refreshToken
? new AsyncEntry(service, refreshKey).setPassword(tokens.refreshToken)
: new AsyncEntry(service, refreshKey).deletePassword();
? new AsyncEntry(SERVICE_NAME, REFRESH_KEY).setPassword(tokens.refreshToken)
: new AsyncEntry(SERVICE_NAME, REFRESH_KEY).deletePassword();

return Promise.all([accessTokenSet, refreshTokenSet]);
}

export async function getStoredTokens(): Promise<StoredTokens | undefined> {
const service = getTokenServiceName();
const accessKey = getAccessTokenKey();
const refreshKey = getRefreshTokenKey();

return Promise.all([
new AsyncEntry(service, accessKey).getPassword(),
new AsyncEntry(service, refreshKey).getPassword(),
new AsyncEntry(SERVICE_NAME, ACCESS_KEY).getPassword(),
new AsyncEntry(SERVICE_NAME, REFRESH_KEY).getPassword(),
]).then(([accessToken, refreshToken]) => {
if (!accessToken && !refreshToken) {
return;
Expand All @@ -42,13 +34,9 @@ export async function getStoredTokens(): Promise<StoredTokens | undefined> {
}

export async function clearStoredTokens() {
const service = getTokenServiceName();
const accessKey = getAccessTokenKey();
const refreshKey = getRefreshTokenKey();

return Promise.all([
new AsyncEntry(service, accessKey).deletePassword(),
new AsyncEntry(service, refreshKey).deletePassword(),
new AsyncEntry(SERVICE_NAME, ACCESS_KEY).deletePassword(),
new AsyncEntry(SERVICE_NAME, REFRESH_KEY).deletePassword(),
]);
}

Expand Down
7 changes: 4 additions & 3 deletions test/commands/auth/logout.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Config } from '@oclif/core';
import { type Mock, vi } from 'vitest';

vi.mock('../../../src/service/auth-token.svc.ts', () => ({
Expand All @@ -22,7 +23,7 @@ describe('AuthLogout command', () => {

it('logs when there are no stored tokens', async () => {
(getStoredTokens as Mock).mockResolvedValue(undefined);
const command = new AuthLogout([], {} as Record<string, unknown>);
const command = new AuthLogout([], {} as unknown as Config);
const logSpy = vi.spyOn(command, 'log').mockImplementation(() => {});

await command.run();
Expand All @@ -33,7 +34,7 @@ describe('AuthLogout command', () => {

it('revokes tokens and clears local storage', async () => {
(getStoredTokens as Mock).mockResolvedValue({ refreshToken: 'refresh', accessToken: 'access' });
const command = new AuthLogout([], {} as Record<string, unknown>);
const command = new AuthLogout([], {} as unknown as Config);
const logSpy = vi.spyOn(command, 'log').mockImplementation(() => {});

await command.run();
Expand All @@ -46,7 +47,7 @@ describe('AuthLogout command', () => {
it('warns when remote logout fails but still clears tokens', async () => {
(getStoredTokens as Mock).mockResolvedValue({ refreshToken: 'refresh' });
(logoutFromProvider as Mock).mockRejectedValueOnce(new Error('network fail'));
const command = new AuthLogout([], {} as Record<string, unknown>);
const command = new AuthLogout([], {} as unknown as Config);
const warnSpy = vi.spyOn(command, 'warn').mockImplementation((msg) => msg);

await command.run();
Expand Down