@herodevs/cli 2.0.0-beta.8 → 2.0.0

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.
Files changed (76) hide show
  1. package/README.md +282 -39
  2. package/bin/main.js +2 -6
  3. package/dist/api/apollo.client.d.ts +3 -0
  4. package/dist/api/apollo.client.js +53 -0
  5. package/dist/api/ci-token.client.d.ts +27 -0
  6. package/dist/api/ci-token.client.js +95 -0
  7. package/dist/api/errors.d.ts +8 -0
  8. package/dist/api/errors.js +13 -0
  9. package/dist/api/gql-operations.d.ts +3 -0
  10. package/dist/api/gql-operations.js +36 -1
  11. package/dist/api/graphql-errors.d.ts +6 -0
  12. package/dist/api/graphql-errors.js +22 -0
  13. package/dist/api/nes.client.d.ts +1 -2
  14. package/dist/api/nes.client.js +40 -16
  15. package/dist/api/user-setup.client.d.ts +18 -0
  16. package/dist/api/user-setup.client.js +92 -0
  17. package/dist/commands/auth/login.d.ts +14 -0
  18. package/dist/commands/auth/login.js +225 -0
  19. package/dist/commands/auth/logout.d.ts +5 -0
  20. package/dist/commands/auth/logout.js +27 -0
  21. package/dist/commands/auth/provision-ci-token.d.ts +5 -0
  22. package/dist/commands/auth/provision-ci-token.js +72 -0
  23. package/dist/commands/report/committers.d.ts +27 -0
  24. package/dist/commands/report/committers.js +215 -0
  25. package/dist/commands/scan/eol.d.ts +7 -0
  26. package/dist/commands/scan/eol.js +120 -32
  27. package/dist/commands/tracker/init.d.ts +14 -0
  28. package/dist/commands/tracker/init.js +84 -0
  29. package/dist/commands/tracker/run.d.ts +15 -0
  30. package/dist/commands/tracker/run.js +183 -0
  31. package/dist/config/constants.d.ts +14 -0
  32. package/dist/config/constants.js +15 -0
  33. package/dist/config/tracker.config.d.ts +16 -0
  34. package/dist/config/tracker.config.js +16 -0
  35. package/dist/hooks/finally/finally.js +13 -7
  36. package/dist/hooks/init/01_initialize_amplitude.js +20 -9
  37. package/dist/service/analytics.svc.d.ts +10 -4
  38. package/dist/service/analytics.svc.js +180 -18
  39. package/dist/service/auth-config.svc.d.ts +2 -0
  40. package/dist/service/auth-config.svc.js +8 -0
  41. package/dist/service/auth-refresh.svc.d.ts +8 -0
  42. package/dist/service/auth-refresh.svc.js +45 -0
  43. package/dist/service/auth-token.svc.d.ts +11 -0
  44. package/dist/service/auth-token.svc.js +62 -0
  45. package/dist/service/auth.svc.d.ts +27 -0
  46. package/dist/service/auth.svc.js +91 -0
  47. package/dist/service/cdx.svc.d.ts +9 -1
  48. package/dist/service/cdx.svc.js +17 -12
  49. package/dist/service/ci-auth.svc.d.ts +6 -0
  50. package/dist/service/ci-auth.svc.js +32 -0
  51. package/dist/service/ci-token.svc.d.ts +6 -0
  52. package/dist/service/ci-token.svc.js +44 -0
  53. package/dist/service/committers.svc.d.ts +58 -0
  54. package/dist/service/committers.svc.js +78 -0
  55. package/dist/service/display.svc.d.ts +8 -0
  56. package/dist/service/display.svc.js +17 -2
  57. package/dist/service/encrypted-store.svc.d.ts +5 -0
  58. package/dist/service/encrypted-store.svc.js +43 -0
  59. package/dist/service/error.svc.d.ts +8 -0
  60. package/dist/service/error.svc.js +28 -0
  61. package/dist/service/file.svc.d.ts +17 -7
  62. package/dist/service/file.svc.js +80 -36
  63. package/dist/service/jwt.svc.d.ts +1 -0
  64. package/dist/service/jwt.svc.js +19 -0
  65. package/dist/service/tracker.svc.d.ts +58 -0
  66. package/dist/service/tracker.svc.js +101 -0
  67. package/dist/types/auth.d.ts +9 -0
  68. package/dist/utils/open-in-browser.d.ts +1 -0
  69. package/dist/utils/open-in-browser.js +21 -0
  70. package/dist/utils/retry.d.ts +11 -0
  71. package/dist/utils/retry.js +29 -0
  72. package/dist/utils/strip-typename.d.ts +1 -0
  73. package/dist/utils/strip-typename.js +16 -0
  74. package/package.json +40 -22
  75. package/dist/service/sbom.worker.js +0 -26
  76. /package/dist/{service/sbom.worker.d.ts → types/auth.js} +0 -0
@@ -1,4 +1,4 @@
1
- import { gql } from '@apollo/client/core/core.cjs';
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
+ }
@@ -1,6 +1,5 @@
1
- import { ApolloClient } from '@apollo/client/core/index.js';
2
1
  import type { CreateEolReportInput, EolReport } from '@herodevs/eol-shared';
3
- export declare const createApollo: (uri: string) => ApolloClient<import("@apollo/client/core/index.js").NormalizedCacheObject>;
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>;
@@ -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
- export const createApollo = (uri) => new ApolloClient({
6
- cache: new InMemoryCache(),
7
- defaultOptions: {
8
- query: { fetchPolicy: 'no-cache' },
9
- },
10
- link: new HttpLink({
11
- uri,
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
- const res = await client.mutate({
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
- const batchResponses = await Promise.all(batch);
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 { ...reportMetadata, components };
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,5 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AuthLogout extends Command {
3
+ static description: string;
4
+ run(): Promise<void>;
5
+ }
@@ -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,5 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AuthProvisionCiToken extends Command {
3
+ static description: string;
4
+ run(): Promise<void>;
5
+ }
@@ -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
+ }