@herodevs/cli 2.0.0-beta.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -38
- package/bin/main.js +2 -6
- package/dist/api/apollo.client.d.ts +3 -0
- package/dist/api/apollo.client.js +53 -0
- package/dist/api/ci-token.client.d.ts +27 -0
- package/dist/api/ci-token.client.js +95 -0
- package/dist/api/errors.d.ts +8 -0
- package/dist/api/errors.js +13 -0
- package/dist/api/gql-operations.d.ts +3 -0
- package/dist/api/gql-operations.js +36 -1
- package/dist/api/graphql-errors.d.ts +6 -0
- package/dist/api/graphql-errors.js +22 -0
- package/dist/api/nes.client.d.ts +1 -2
- package/dist/api/nes.client.js +40 -16
- package/dist/api/user-setup.client.d.ts +18 -0
- package/dist/api/user-setup.client.js +92 -0
- package/dist/commands/auth/login.d.ts +14 -0
- package/dist/commands/auth/login.js +225 -0
- package/dist/commands/auth/logout.d.ts +5 -0
- package/dist/commands/auth/logout.js +27 -0
- package/dist/commands/auth/provision-ci-token.d.ts +5 -0
- package/dist/commands/auth/provision-ci-token.js +72 -0
- package/dist/commands/report/committers.d.ts +27 -0
- package/dist/commands/report/committers.js +215 -0
- package/dist/commands/scan/eol.d.ts +7 -0
- package/dist/commands/scan/eol.js +113 -25
- package/dist/commands/tracker/init.d.ts +14 -0
- package/dist/commands/tracker/init.js +84 -0
- package/dist/commands/tracker/run.d.ts +15 -0
- package/dist/commands/tracker/run.js +183 -0
- package/dist/config/constants.d.ts +14 -0
- package/dist/config/constants.js +15 -0
- package/dist/config/tracker.config.d.ts +16 -0
- package/dist/config/tracker.config.js +16 -0
- package/dist/hooks/finally/finally.js +13 -7
- package/dist/hooks/init/01_initialize_amplitude.js +20 -9
- package/dist/service/analytics.svc.d.ts +10 -4
- package/dist/service/analytics.svc.js +180 -18
- package/dist/service/auth-config.svc.d.ts +2 -0
- package/dist/service/auth-config.svc.js +8 -0
- package/dist/service/auth-refresh.svc.d.ts +8 -0
- package/dist/service/auth-refresh.svc.js +45 -0
- package/dist/service/auth-token.svc.d.ts +11 -0
- package/dist/service/auth-token.svc.js +62 -0
- package/dist/service/auth.svc.d.ts +27 -0
- package/dist/service/auth.svc.js +91 -0
- package/dist/service/cdx.svc.d.ts +9 -1
- package/dist/service/cdx.svc.js +17 -12
- package/dist/service/ci-auth.svc.d.ts +6 -0
- package/dist/service/ci-auth.svc.js +32 -0
- package/dist/service/ci-token.svc.d.ts +6 -0
- package/dist/service/ci-token.svc.js +44 -0
- package/dist/service/committers.svc.d.ts +58 -0
- package/dist/service/committers.svc.js +78 -0
- package/dist/service/display.svc.d.ts +8 -0
- package/dist/service/display.svc.js +17 -2
- package/dist/service/encrypted-store.svc.d.ts +5 -0
- package/dist/service/encrypted-store.svc.js +43 -0
- package/dist/service/error.svc.d.ts +8 -0
- package/dist/service/error.svc.js +28 -0
- package/dist/service/file.svc.d.ts +17 -7
- package/dist/service/file.svc.js +80 -36
- package/dist/service/jwt.svc.d.ts +1 -0
- package/dist/service/jwt.svc.js +19 -0
- package/dist/service/tracker.svc.d.ts +58 -0
- package/dist/service/tracker.svc.js +101 -0
- package/dist/types/auth.d.ts +9 -0
- package/dist/utils/open-in-browser.d.ts +1 -0
- package/dist/utils/open-in-browser.js +21 -0
- package/dist/utils/retry.d.ts +11 -0
- package/dist/utils/retry.js +29 -0
- package/dist/utils/strip-typename.d.ts +1 -0
- package/dist/utils/strip-typename.js +16 -0
- package/package.json +38 -21
- package/dist/service/sbom.worker.js +0 -26
- /package/dist/{service/sbom.worker.d.ts → types/auth.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { gql } from '@apollo/client/core
|
|
1
|
+
import { gql } from '@apollo/client/core';
|
|
2
2
|
export const createReportMutation = gql `
|
|
3
3
|
mutation createReport($input: CreateEolReportInput) {
|
|
4
4
|
eol {
|
|
@@ -20,6 +20,7 @@ query GetEolReport($input: GetEolReportInput) {
|
|
|
20
20
|
components {
|
|
21
21
|
purl
|
|
22
22
|
metadata
|
|
23
|
+
dependencySummary
|
|
23
24
|
nesRemediation {
|
|
24
25
|
remediations {
|
|
25
26
|
urls {
|
|
@@ -34,3 +35,37 @@ query GetEolReport($input: GetEolReportInput) {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
`;
|
|
38
|
+
export const userSetupStatusQuery = gql `
|
|
39
|
+
query Eol {
|
|
40
|
+
eol {
|
|
41
|
+
userSetupStatus {
|
|
42
|
+
isComplete
|
|
43
|
+
orgId
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
export const completeUserSetupMutation = gql `
|
|
49
|
+
mutation Eol {
|
|
50
|
+
eol {
|
|
51
|
+
completeUserSetup {
|
|
52
|
+
isComplete
|
|
53
|
+
orgId
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
export const getOrgAccessTokensMutation = gql `
|
|
59
|
+
mutation GetOrgAccessTokens(
|
|
60
|
+
$input: IamAccessOrgTokensInput!
|
|
61
|
+
) {
|
|
62
|
+
iamV2 {
|
|
63
|
+
access {
|
|
64
|
+
getOrgAccessTokens(input: $input) {
|
|
65
|
+
accessToken
|
|
66
|
+
refreshToken
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { GraphQLFormattedError } from 'graphql';
|
|
2
|
+
export type GraphQLErrorResult = {
|
|
3
|
+
error?: unknown;
|
|
4
|
+
errors?: ReadonlyArray<GraphQLFormattedError>;
|
|
5
|
+
};
|
|
6
|
+
export declare function getGraphQLErrors(result: GraphQLErrorResult): ReadonlyArray<GraphQLFormattedError> | undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function getGraphQLErrors(result) {
|
|
2
|
+
if (result.errors?.length) {
|
|
3
|
+
return result.errors;
|
|
4
|
+
}
|
|
5
|
+
const error = result.error;
|
|
6
|
+
if (!error || typeof error !== 'object') {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if ('errors' in error) {
|
|
10
|
+
const errors = error.errors;
|
|
11
|
+
if (errors?.length) {
|
|
12
|
+
return errors;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if ('graphQLErrors' in error) {
|
|
16
|
+
const errors = error.graphQLErrors;
|
|
17
|
+
if (errors?.length) {
|
|
18
|
+
return errors;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
package/dist/api/nes.client.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { ApolloClient } from '@apollo/client/core/index.js';
|
|
2
1
|
import type { CreateEolReportInput, EolReport } from '@herodevs/eol-shared';
|
|
3
|
-
|
|
2
|
+
import { createApollo } from './apollo.client.ts';
|
|
4
3
|
export declare const SbomScanner: (client: ReturnType<typeof createApollo>) => (input: CreateEolReportInput) => Promise<EolReport>;
|
|
5
4
|
export declare class NesClient {
|
|
6
5
|
startScan: ReturnType<typeof SbomScanner>;
|
package/dist/api/nes.client.js
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
|
-
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client/core/index.js';
|
|
2
1
|
import { config } from "../config/constants.js";
|
|
3
2
|
import { debugLogger } from "../service/log.svc.js";
|
|
3
|
+
import { stripTypename } from "../utils/strip-typename.js";
|
|
4
|
+
import { createApollo } from "./apollo.client.js";
|
|
5
|
+
import { ApiError, isApiErrorCode } from "./errors.js";
|
|
4
6
|
import { createReportMutation, getEolReportQuery } from "./gql-operations.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
headers: {
|
|
13
|
-
'User-Agent': `hdcli/${process.env.npm_package_version ?? 'unknown'}`,
|
|
14
|
-
},
|
|
15
|
-
}),
|
|
16
|
-
});
|
|
7
|
+
import { getGraphQLErrors } from "./graphql-errors.js";
|
|
8
|
+
function extractErrorCode(errors) {
|
|
9
|
+
const code = errors[0]?.extensions?.code;
|
|
10
|
+
if (!code || !isApiErrorCode(code))
|
|
11
|
+
return;
|
|
12
|
+
return code;
|
|
13
|
+
}
|
|
17
14
|
export const SbomScanner = (client) => {
|
|
18
15
|
return async (input) => {
|
|
19
|
-
|
|
16
|
+
let res;
|
|
17
|
+
res = await client.mutate({
|
|
20
18
|
mutation: createReportMutation,
|
|
21
19
|
variables: { input },
|
|
22
20
|
});
|
|
21
|
+
const errors = getGraphQLErrors(res);
|
|
22
|
+
if (res?.error || errors?.length) {
|
|
23
|
+
debugLogger('Error returned from createReport mutation: %o', res.error || errors);
|
|
24
|
+
if (errors?.length) {
|
|
25
|
+
const code = extractErrorCode(errors);
|
|
26
|
+
if (code) {
|
|
27
|
+
throw new ApiError(errors[0].message, code);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw new Error('Failed to create EOL report');
|
|
31
|
+
}
|
|
23
32
|
const result = res.data?.eol?.createReport;
|
|
24
33
|
if (!result?.success || !result.id) {
|
|
25
34
|
debugLogger('failed scan %o', result || {});
|
|
@@ -41,8 +50,20 @@ export const SbomScanner = (client) => {
|
|
|
41
50
|
let reportMetadata = null;
|
|
42
51
|
for (let i = 0; i < pages.length; i += config.concurrentPageRequests) {
|
|
43
52
|
const batch = pages.slice(i, i + config.concurrentPageRequests);
|
|
44
|
-
|
|
53
|
+
let batchResponses;
|
|
54
|
+
batchResponses = await Promise.all(batch);
|
|
45
55
|
for (const response of batchResponses) {
|
|
56
|
+
const queryErrors = getGraphQLErrors(response);
|
|
57
|
+
if (response?.error || queryErrors?.length || !response.data?.eol) {
|
|
58
|
+
debugLogger('Error in getReport query response: %o', response?.error ?? queryErrors ?? response);
|
|
59
|
+
if (queryErrors?.length) {
|
|
60
|
+
const code = extractErrorCode(queryErrors);
|
|
61
|
+
if (code) {
|
|
62
|
+
throw new ApiError(queryErrors[0].message, code);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw new Error('Failed to fetch EOL report');
|
|
66
|
+
}
|
|
46
67
|
const report = response.data.eol.report;
|
|
47
68
|
reportMetadata ??= report;
|
|
48
69
|
components.push(...(report?.components ?? []));
|
|
@@ -51,7 +72,10 @@ export const SbomScanner = (client) => {
|
|
|
51
72
|
if (!reportMetadata) {
|
|
52
73
|
throw new Error('Failed to fetch EOL report');
|
|
53
74
|
}
|
|
54
|
-
return {
|
|
75
|
+
return stripTypename({
|
|
76
|
+
...reportMetadata,
|
|
77
|
+
components,
|
|
78
|
+
});
|
|
55
79
|
};
|
|
56
80
|
};
|
|
57
81
|
export class NesClient {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function getUserSetupStatus(options?: {
|
|
2
|
+
preferOAuth?: boolean;
|
|
3
|
+
orgAccessToken?: string;
|
|
4
|
+
}): Promise<{
|
|
5
|
+
isComplete: boolean;
|
|
6
|
+
orgId?: number | null;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function completeUserSetup(options?: {
|
|
9
|
+
preferOAuth?: boolean;
|
|
10
|
+
orgAccessToken?: string;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
isComplete: boolean;
|
|
13
|
+
orgId?: number | null;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function ensureUserSetup(options?: {
|
|
16
|
+
preferOAuth?: boolean;
|
|
17
|
+
orgAccessToken?: string;
|
|
18
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { config } from "../config/constants.js";
|
|
2
|
+
import { getTokenProvider } from "../service/auth.svc.js";
|
|
3
|
+
import { debugLogger } from "../service/log.svc.js";
|
|
4
|
+
import { withRetries } from "../utils/retry.js";
|
|
5
|
+
import { createApollo } from "./apollo.client.js";
|
|
6
|
+
import { ApiError, isApiErrorCode } from "./errors.js";
|
|
7
|
+
import { completeUserSetupMutation, userSetupStatusQuery } from "./gql-operations.js";
|
|
8
|
+
import { getGraphQLErrors } from "./graphql-errors.js";
|
|
9
|
+
const USER_SETUP_MAX_ATTEMPTS = 3;
|
|
10
|
+
const USER_SETUP_RETRY_DELAY_MS = 500;
|
|
11
|
+
const USER_FACING_SERVER_ERROR = 'Please contact support@herodevs.com.';
|
|
12
|
+
const SERVER_ERROR_CODES = ['INTERNAL_SERVER_ERROR', 'SERVER_ERROR', 'SERVICE_UNAVAILABLE'];
|
|
13
|
+
const getGraphqlUrl = () => `${config.graphqlHost}${config.graphqlPath}`;
|
|
14
|
+
function extractErrorCode(errors) {
|
|
15
|
+
const code = errors[0]?.extensions?.code;
|
|
16
|
+
if (!code || !isApiErrorCode(code))
|
|
17
|
+
return;
|
|
18
|
+
return code;
|
|
19
|
+
}
|
|
20
|
+
export async function getUserSetupStatus(options) {
|
|
21
|
+
const tokenProvider = getTokenProvider(options?.preferOAuth, options?.orgAccessToken);
|
|
22
|
+
const client = createApollo(getGraphqlUrl(), tokenProvider);
|
|
23
|
+
const res = await client.query({ query: userSetupStatusQuery });
|
|
24
|
+
const errors = getGraphQLErrors(res);
|
|
25
|
+
if (res?.error || errors?.length) {
|
|
26
|
+
debugLogger('Error returned from userSetupStatus query: %o', res.error || errors);
|
|
27
|
+
if (errors?.length) {
|
|
28
|
+
const rawCode = errors[0]?.extensions?.code;
|
|
29
|
+
if (rawCode && SERVER_ERROR_CODES.includes(rawCode)) {
|
|
30
|
+
throw new Error(USER_FACING_SERVER_ERROR);
|
|
31
|
+
}
|
|
32
|
+
const code = extractErrorCode(errors);
|
|
33
|
+
const message = errors[0].message ?? 'Failed to check user setup status';
|
|
34
|
+
if (code) {
|
|
35
|
+
throw new ApiError(message, code);
|
|
36
|
+
}
|
|
37
|
+
throw new Error(message);
|
|
38
|
+
}
|
|
39
|
+
throw new Error('Failed to check user setup status');
|
|
40
|
+
}
|
|
41
|
+
const status = res.data?.eol?.userSetupStatus;
|
|
42
|
+
if (!status || typeof status.isComplete !== 'boolean') {
|
|
43
|
+
debugLogger('Unexpected userSetupStatus query response: %o', res.data);
|
|
44
|
+
throw new Error('Failed to check user setup status');
|
|
45
|
+
}
|
|
46
|
+
return { isComplete: status.isComplete, orgId: status.orgId ?? undefined };
|
|
47
|
+
}
|
|
48
|
+
export async function completeUserSetup(options) {
|
|
49
|
+
const tokenProvider = getTokenProvider(options?.preferOAuth, options?.orgAccessToken);
|
|
50
|
+
const client = createApollo(getGraphqlUrl(), tokenProvider);
|
|
51
|
+
const res = await client.mutate({ mutation: completeUserSetupMutation });
|
|
52
|
+
const errors = getGraphQLErrors(res);
|
|
53
|
+
if (res?.error || errors?.length) {
|
|
54
|
+
debugLogger('Error returned from completeUserSetup mutation: %o', res.error || errors);
|
|
55
|
+
if (errors?.length) {
|
|
56
|
+
const rawCode = errors[0]?.extensions?.code;
|
|
57
|
+
if (rawCode && SERVER_ERROR_CODES.includes(rawCode)) {
|
|
58
|
+
throw new Error(USER_FACING_SERVER_ERROR);
|
|
59
|
+
}
|
|
60
|
+
const code = extractErrorCode(errors);
|
|
61
|
+
const message = errors[0].message ?? 'Failed to complete user setup';
|
|
62
|
+
if (code) {
|
|
63
|
+
throw new ApiError(message, code);
|
|
64
|
+
}
|
|
65
|
+
throw new Error(message);
|
|
66
|
+
}
|
|
67
|
+
throw new Error('Failed to complete user setup');
|
|
68
|
+
}
|
|
69
|
+
const result = res.data?.eol?.completeUserSetup;
|
|
70
|
+
if (!result || result.isComplete !== true) {
|
|
71
|
+
debugLogger('completeUserSetup mutation returned unsuccessful response: %o', res.data);
|
|
72
|
+
throw new Error('Failed to complete user setup');
|
|
73
|
+
}
|
|
74
|
+
return { isComplete: true, orgId: result.orgId ?? undefined };
|
|
75
|
+
}
|
|
76
|
+
export async function ensureUserSetup(options) {
|
|
77
|
+
const status = await withRetries('user-setup-status', () => getUserSetupStatus(options), {
|
|
78
|
+
attempts: USER_SETUP_MAX_ATTEMPTS,
|
|
79
|
+
baseDelayMs: USER_SETUP_RETRY_DELAY_MS,
|
|
80
|
+
});
|
|
81
|
+
if (status.isComplete && status.orgId != null) {
|
|
82
|
+
return status.orgId;
|
|
83
|
+
}
|
|
84
|
+
const result = await withRetries('user-setup-complete', () => completeUserSetup(options), {
|
|
85
|
+
attempts: USER_SETUP_MAX_ATTEMPTS,
|
|
86
|
+
baseDelayMs: USER_SETUP_RETRY_DELAY_MS,
|
|
87
|
+
});
|
|
88
|
+
if (result.orgId != null) {
|
|
89
|
+
return result.orgId;
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`User setup did not return an organization ID. ${USER_FACING_SERVER_ERROR}`);
|
|
92
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class AuthLogin extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
private server?;
|
|
5
|
+
private stopServerPromise?;
|
|
6
|
+
private readonly port;
|
|
7
|
+
private readonly redirectUri;
|
|
8
|
+
private readonly realmUrl;
|
|
9
|
+
private readonly clientId;
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
private startServerAndAwaitCode;
|
|
12
|
+
private stopServer;
|
|
13
|
+
private exchangeCodeForToken;
|
|
14
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { URL } from 'node:url';
|
|
5
|
+
import { Command } from '@oclif/core';
|
|
6
|
+
import { ensureUserSetup } from "../../api/user-setup.client.js";
|
|
7
|
+
import { OAUTH_CALLBACK_ERROR_CODES } from "../../config/constants.js";
|
|
8
|
+
import { refreshIdentityFromStoredToken } from "../../service/analytics.svc.js";
|
|
9
|
+
import { persistTokenResponse } from "../../service/auth.svc.js";
|
|
10
|
+
import { getClientId, getRealmUrl } from "../../service/auth-config.svc.js";
|
|
11
|
+
import { debugLogger, getErrorMessage } from "../../service/log.svc.js";
|
|
12
|
+
import { openInBrowser } from "../../utils/open-in-browser.js";
|
|
13
|
+
export default class AuthLogin extends Command {
|
|
14
|
+
static description = 'OAuth CLI login';
|
|
15
|
+
server;
|
|
16
|
+
stopServerPromise;
|
|
17
|
+
port = parseInt(process.env.OAUTH_CALLBACK_PORT || '4000', 10);
|
|
18
|
+
redirectUri = process.env.OAUTH_CALLBACK_REDIRECT || `http://localhost:${this.port}/oauth2/callback`;
|
|
19
|
+
realmUrl = getRealmUrl();
|
|
20
|
+
clientId = getClientId();
|
|
21
|
+
async run() {
|
|
22
|
+
if (typeof this.config.runHook === 'function') {
|
|
23
|
+
await this.parse(AuthLogin);
|
|
24
|
+
}
|
|
25
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
26
|
+
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
27
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
28
|
+
const authUrl = `${this.realmUrl}/auth?` +
|
|
29
|
+
`client_id=${this.clientId}` +
|
|
30
|
+
`&response_type=code` +
|
|
31
|
+
`&redirect_uri=${encodeURIComponent(this.redirectUri)}` +
|
|
32
|
+
`&code_challenge=${codeChallenge}` +
|
|
33
|
+
`&code_challenge_method=S256` +
|
|
34
|
+
`&state=${state}`;
|
|
35
|
+
const code = await this.startServerAndAwaitCode(authUrl, state);
|
|
36
|
+
const token = await this.exchangeCodeForToken(code, codeVerifier);
|
|
37
|
+
try {
|
|
38
|
+
await persistTokenResponse(token);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
this.warn(`Failed to store tokens securely: ${error instanceof Error ? error.message : error}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await ensureUserSetup({ preferOAuth: true });
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
this.error(`User setup failed. ${getErrorMessage(error)}`);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await refreshIdentityFromStoredToken();
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
this.warn(`Failed to refresh analytics identity: ${getErrorMessage(error)}`);
|
|
55
|
+
}
|
|
56
|
+
this.log('\nLogin completed successfully.');
|
|
57
|
+
}
|
|
58
|
+
startServerAndAwaitCode(authUrl, expectedState) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
this.server = http.createServer((req, res) => {
|
|
61
|
+
if (!req.url) {
|
|
62
|
+
res.writeHead(400);
|
|
63
|
+
res.end('Invalid request');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let parsedUrl;
|
|
67
|
+
try {
|
|
68
|
+
parsedUrl = new URL(req.url, `http://localhost:${this.port}`);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
72
|
+
res.end('Invalid callback URL');
|
|
73
|
+
this.stopServer();
|
|
74
|
+
reject(new Error('Invalid callback URL'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (parsedUrl.pathname === '/oauth2/callback') {
|
|
78
|
+
const code = parsedUrl.searchParams.get('code');
|
|
79
|
+
const state = parsedUrl.searchParams.get('state');
|
|
80
|
+
const oauthError = parsedUrl.searchParams.get('error');
|
|
81
|
+
const oauthErrorDescription = parsedUrl.searchParams.get('error_description');
|
|
82
|
+
if (!state) {
|
|
83
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
84
|
+
res.end('Missing state parameter.');
|
|
85
|
+
this.stopServer();
|
|
86
|
+
return reject(new Error('Missing state parameter in callback'));
|
|
87
|
+
}
|
|
88
|
+
if (state !== expectedState) {
|
|
89
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
90
|
+
res.end('State verification failed. Please restart the login flow.');
|
|
91
|
+
this.stopServer();
|
|
92
|
+
return reject(new Error('State verification failed'));
|
|
93
|
+
}
|
|
94
|
+
if (oauthError) {
|
|
95
|
+
const isAlreadyLoggedIn = oauthError === OAUTH_CALLBACK_ERROR_CODES.ALREADY_LOGGED_IN;
|
|
96
|
+
const isDifferentUserAuthenticated = oauthError === OAUTH_CALLBACK_ERROR_CODES.DIFFERENT_USER_AUTHENTICATED;
|
|
97
|
+
debugLogger('OAuth callback returned error: %s (%s)', oauthError, oauthErrorDescription ?? 'no description');
|
|
98
|
+
let browserMessage;
|
|
99
|
+
let cliErrorMessage;
|
|
100
|
+
if (isAlreadyLoggedIn) {
|
|
101
|
+
browserMessage = "You're already signed in. We'll continue for you. Return to the terminal.";
|
|
102
|
+
cliErrorMessage = `You're already signed in. Run "hd auth login" again to continue.`;
|
|
103
|
+
}
|
|
104
|
+
else if (isDifferentUserAuthenticated) {
|
|
105
|
+
browserMessage =
|
|
106
|
+
"You're signed in with a different account than this sign-in attempt. Return to the terminal.";
|
|
107
|
+
cliErrorMessage =
|
|
108
|
+
`You're signed in with a different account than this sign-in attempt. ` +
|
|
109
|
+
`Choose another account, or reset this sign-in session and try again. ` +
|
|
110
|
+
`If needed, run "hd auth logout" and then "hd auth login".`;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
browserMessage = "We couldn't complete sign-in. Return to the terminal and try again.";
|
|
114
|
+
cliErrorMessage = `We couldn't complete sign-in. Please run "hd auth login" again.`;
|
|
115
|
+
}
|
|
116
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
117
|
+
res.end(browserMessage);
|
|
118
|
+
this.stopServer();
|
|
119
|
+
return reject(new Error(cliErrorMessage));
|
|
120
|
+
}
|
|
121
|
+
if (code) {
|
|
122
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
123
|
+
res.end('Login successful. You can close this window.');
|
|
124
|
+
this.stopServer();
|
|
125
|
+
resolve(code);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
129
|
+
res.end('No authorization code returned. Please try again.');
|
|
130
|
+
this.stopServer();
|
|
131
|
+
reject(new Error('No code returned from Keycloak'));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
res.writeHead(404);
|
|
136
|
+
res.end();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
this.server.listen(this.port, async () => {
|
|
140
|
+
await new Promise((resolve) => {
|
|
141
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
142
|
+
rl.question(`Press Enter to navigate to: ${authUrl}\n`, () => {
|
|
143
|
+
rl.close();
|
|
144
|
+
resolve();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
try {
|
|
148
|
+
await openInBrowser(authUrl);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
this.warn(`Failed to open browser automatically. Please open this URL manually:\n${authUrl}\n${err instanceof Error ? err.message : err}`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
this.server.on('error', (err) => {
|
|
155
|
+
this.stopServer();
|
|
156
|
+
reject(err);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
stopServer() {
|
|
161
|
+
if (this.stopServerPromise) {
|
|
162
|
+
return this.stopServerPromise;
|
|
163
|
+
}
|
|
164
|
+
const server = this.server;
|
|
165
|
+
this.server = undefined;
|
|
166
|
+
if (!server) {
|
|
167
|
+
return Promise.resolve();
|
|
168
|
+
}
|
|
169
|
+
const stopPromise = new Promise((resolve) => {
|
|
170
|
+
const timeoutMs = 1000;
|
|
171
|
+
let settled = false;
|
|
172
|
+
let timeout;
|
|
173
|
+
const complete = (err) => {
|
|
174
|
+
if (settled) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
settled = true;
|
|
178
|
+
if (timeout) {
|
|
179
|
+
clearTimeout(timeout);
|
|
180
|
+
timeout = undefined;
|
|
181
|
+
}
|
|
182
|
+
const code = err?.code;
|
|
183
|
+
if (err && code !== 'ERR_SERVER_NOT_RUNNING') {
|
|
184
|
+
this.warn('Failed to stop local OAuth callback server.');
|
|
185
|
+
debugLogger('Failed to stop local OAuth callback server: %s', getErrorMessage(err));
|
|
186
|
+
}
|
|
187
|
+
resolve();
|
|
188
|
+
};
|
|
189
|
+
timeout = setTimeout(() => {
|
|
190
|
+
debugLogger('Timed out while stopping local OAuth callback server after %dms', timeoutMs);
|
|
191
|
+
complete();
|
|
192
|
+
}, timeoutMs);
|
|
193
|
+
try {
|
|
194
|
+
server.close((err) => complete(err));
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
complete(err);
|
|
198
|
+
}
|
|
199
|
+
}).finally(() => {
|
|
200
|
+
this.stopServerPromise = undefined;
|
|
201
|
+
});
|
|
202
|
+
this.stopServerPromise = stopPromise;
|
|
203
|
+
return stopPromise;
|
|
204
|
+
}
|
|
205
|
+
async exchangeCodeForToken(code, codeVerifier) {
|
|
206
|
+
const tokenUrl = `${this.realmUrl}/token`;
|
|
207
|
+
const params = new URLSearchParams({
|
|
208
|
+
grant_type: 'authorization_code',
|
|
209
|
+
client_id: this.clientId,
|
|
210
|
+
redirect_uri: this.redirectUri,
|
|
211
|
+
code_verifier: codeVerifier,
|
|
212
|
+
code,
|
|
213
|
+
});
|
|
214
|
+
const response = await fetch(tokenUrl, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
217
|
+
body: params.toString(),
|
|
218
|
+
});
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
const text = await response.text();
|
|
221
|
+
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}\n${text}`);
|
|
222
|
+
}
|
|
223
|
+
return response.json();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { clearTrackedIdentity } from "../../service/analytics.svc.js";
|
|
3
|
+
import { logoutFromProvider } from "../../service/auth-refresh.svc.js";
|
|
4
|
+
import { clearStoredTokens, getStoredTokens } from "../../service/auth-token.svc.js";
|
|
5
|
+
export default class AuthLogout extends Command {
|
|
6
|
+
static description = 'Logs out of HeroDevs OAuth and clears stored tokens';
|
|
7
|
+
async run() {
|
|
8
|
+
if (typeof this.config.runHook === 'function') {
|
|
9
|
+
await this.parse(AuthLogout);
|
|
10
|
+
}
|
|
11
|
+
const tokens = await getStoredTokens();
|
|
12
|
+
if (!tokens?.accessToken && !tokens?.refreshToken) {
|
|
13
|
+
this.log('No stored authentication tokens found.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
await logoutFromProvider(tokens?.refreshToken);
|
|
18
|
+
this.log('Logged out of HeroDevs OAuth provider.');
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
this.warn(`Failed to revoke tokens remotely: ${error instanceof Error ? error.message : error}`);
|
|
22
|
+
}
|
|
23
|
+
clearTrackedIdentity();
|
|
24
|
+
await clearStoredTokens();
|
|
25
|
+
this.log('Local authentication tokens removed from your system.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { provisionCIToken } from "../../api/ci-token.client.js";
|
|
3
|
+
import { ensureUserSetup } from "../../api/user-setup.client.js";
|
|
4
|
+
import { refreshIdentityFromStoredToken, track } from "../../service/analytics.svc.js";
|
|
5
|
+
import { requireAccessToken } from "../../service/auth.svc.js";
|
|
6
|
+
import { saveCIToken } from "../../service/ci-token.svc.js";
|
|
7
|
+
import { getErrorMessage } from "../../service/log.svc.js";
|
|
8
|
+
export default class AuthProvisionCiToken extends Command {
|
|
9
|
+
static description = 'Provision a CI/CD long-lived refresh token for headless auth';
|
|
10
|
+
async run() {
|
|
11
|
+
await this.parse(AuthProvisionCiToken);
|
|
12
|
+
try {
|
|
13
|
+
await requireAccessToken();
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
this.error(`Must be logged in to provision CI token. Run 'hd auth login' first. ${getErrorMessage(error)}`);
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
await refreshIdentityFromStoredToken();
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
this.warn(`Failed to refresh analytics identity: ${getErrorMessage(error)}`);
|
|
23
|
+
}
|
|
24
|
+
track('CLI CI Token Provision Started', (context) => ({
|
|
25
|
+
command: 'auth provision-ci-token',
|
|
26
|
+
app_used: context.app_used,
|
|
27
|
+
ci_provider: context.ci_provider,
|
|
28
|
+
cli_version: context.cli_version,
|
|
29
|
+
started_at: context.started_at,
|
|
30
|
+
}));
|
|
31
|
+
let orgId;
|
|
32
|
+
try {
|
|
33
|
+
orgId = await ensureUserSetup();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
track('CLI CI Token Provision Failed', () => ({
|
|
37
|
+
command: 'auth provision-ci-token',
|
|
38
|
+
error: `user_setup_failed:${getErrorMessage(error)}`,
|
|
39
|
+
}));
|
|
40
|
+
this.error(`User setup failed. ${getErrorMessage(error)}`);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const result = await provisionCIToken({ orgId });
|
|
44
|
+
try {
|
|
45
|
+
await ensureUserSetup({ orgAccessToken: result.access_token });
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
track('CLI CI Token Provision Failed', () => ({
|
|
49
|
+
command: 'auth provision-ci-token',
|
|
50
|
+
error: `user_setup_failed:${getErrorMessage(error)}`,
|
|
51
|
+
}));
|
|
52
|
+
this.error(`User Org setup failed. ${getErrorMessage(error)}`);
|
|
53
|
+
}
|
|
54
|
+
const refreshToken = result.refresh_token;
|
|
55
|
+
saveCIToken(refreshToken);
|
|
56
|
+
this.log('CI token provisioned and saved locally.');
|
|
57
|
+
this.log('');
|
|
58
|
+
this.log('For CI/CD, set this environment variable:');
|
|
59
|
+
this.log(` HD_CI_CREDENTIAL=${refreshToken}`);
|
|
60
|
+
track('CLI CI Token Provision Succeeded', () => ({
|
|
61
|
+
command: 'auth provision-ci-token',
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
track('CLI CI Token Provision Failed', () => ({
|
|
66
|
+
command: 'auth provision-ci-token',
|
|
67
|
+
error: `provision_failed:${getErrorMessage(error)}`,
|
|
68
|
+
}));
|
|
69
|
+
this.error(`CI token provisioning failed. ${getErrorMessage(error)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { type CommittersReport } from '../../service/committers.svc.ts';
|
|
3
|
+
export default class Committers extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static flags: {
|
|
8
|
+
beforeDate: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
afterDate: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
exclude: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
directory: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
monthly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
months: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
save: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
};
|
|
18
|
+
run(): Promise<CommittersReport | string>;
|
|
19
|
+
/**
|
|
20
|
+
* Fetches git commit data with month and author information
|
|
21
|
+
* @param sinceDate - Date range for git log
|
|
22
|
+
* @param beforeDateEndOfDay - End date for git log
|
|
23
|
+
* @param ignores - indicate elements to exclude for git log
|
|
24
|
+
* @param cwd - directory to use for git log
|
|
25
|
+
*/
|
|
26
|
+
private fetchGitCommitData;
|
|
27
|
+
}
|