@herodevs/cli 2.0.0-beta.13 → 2.0.0-beta.15

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 (63) hide show
  1. package/README.md +192 -20
  2. package/dist/api/apollo.client.d.ts +3 -0
  3. package/dist/api/apollo.client.js +53 -0
  4. package/dist/api/ci-token.client.d.ts +26 -0
  5. package/dist/api/ci-token.client.js +95 -0
  6. package/dist/api/errors.d.ts +8 -0
  7. package/dist/api/errors.js +13 -0
  8. package/dist/api/gql-operations.d.ts +3 -0
  9. package/dist/api/gql-operations.js +36 -1
  10. package/dist/api/graphql-errors.d.ts +6 -0
  11. package/dist/api/graphql-errors.js +22 -0
  12. package/dist/api/nes.client.d.ts +1 -2
  13. package/dist/api/nes.client.js +31 -20
  14. package/dist/api/user-setup.client.d.ts +15 -0
  15. package/dist/api/user-setup.client.js +92 -0
  16. package/dist/commands/auth/login.d.ts +14 -0
  17. package/dist/commands/auth/login.js +225 -0
  18. package/dist/commands/auth/logout.d.ts +5 -0
  19. package/dist/commands/auth/logout.js +27 -0
  20. package/dist/commands/auth/provision-ci-token.d.ts +5 -0
  21. package/dist/commands/auth/provision-ci-token.js +62 -0
  22. package/dist/commands/report/committers.d.ts +11 -7
  23. package/dist/commands/report/committers.js +144 -76
  24. package/dist/commands/scan/eol.d.ts +2 -0
  25. package/dist/commands/scan/eol.js +34 -4
  26. package/dist/commands/tracker/init.d.ts +14 -0
  27. package/dist/commands/tracker/init.js +84 -0
  28. package/dist/commands/tracker/run.d.ts +15 -0
  29. package/dist/commands/tracker/run.js +183 -0
  30. package/dist/config/constants.d.ts +14 -0
  31. package/dist/config/constants.js +15 -0
  32. package/dist/config/tracker.config.d.ts +16 -0
  33. package/dist/config/tracker.config.js +16 -0
  34. package/dist/hooks/finally/finally.js +10 -4
  35. package/dist/hooks/init/01_initialize_amplitude.js +20 -9
  36. package/dist/service/analytics.svc.d.ts +10 -3
  37. package/dist/service/analytics.svc.js +180 -18
  38. package/dist/service/auth-config.svc.d.ts +5 -0
  39. package/dist/service/auth-config.svc.js +20 -0
  40. package/dist/service/auth-refresh.svc.d.ts +8 -0
  41. package/dist/service/auth-refresh.svc.js +45 -0
  42. package/dist/service/auth-token.svc.d.ts +11 -0
  43. package/dist/service/auth-token.svc.js +48 -0
  44. package/dist/service/auth.svc.d.ts +27 -0
  45. package/dist/service/auth.svc.js +88 -0
  46. package/dist/service/ci-auth.svc.d.ts +6 -0
  47. package/dist/service/ci-auth.svc.js +32 -0
  48. package/dist/service/ci-token.svc.d.ts +6 -0
  49. package/dist/service/ci-token.svc.js +75 -0
  50. package/dist/service/committers.svc.d.ts +46 -58
  51. package/dist/service/committers.svc.js +55 -173
  52. package/dist/service/jwt.svc.d.ts +1 -0
  53. package/dist/service/jwt.svc.js +19 -0
  54. package/dist/service/tracker.svc.d.ts +58 -0
  55. package/dist/service/tracker.svc.js +101 -0
  56. package/dist/types/auth.d.ts +9 -0
  57. package/dist/types/auth.js +1 -0
  58. package/dist/utils/open-in-browser.d.ts +1 -0
  59. package/dist/utils/open-in-browser.js +21 -0
  60. package/dist/utils/retry.d.ts +11 -0
  61. package/dist/utils/retry.js +29 -0
  62. package/dist/utils/strip-typename.js +2 -1
  63. package/package.json +38 -19
package/README.md CHANGED
@@ -43,11 +43,11 @@ npm install -g @herodevs/cli@beta
43
43
  HeroDevs CLI is available as a binary installation, without requiring `npm`. To do that, you may either download and run the script manually, or use the following cURL or Wget command:
44
44
 
45
45
  ```sh
46
- curl -o- https://raw.githubusercontent.com/herodevs/cli/v2.0.0-beta.13/scripts/install.sh | bash
46
+ curl -o- https://raw.githubusercontent.com/herodevs/cli/v2.0.0-beta.15/scripts/install.sh | bash
47
47
  ```
48
48
 
49
49
  ```sh
50
- wget -qO- https://raw.githubusercontent.com/herodevs/cli/v2.0.0-beta.13/scripts/install.sh | bash
50
+ wget -qO- https://raw.githubusercontent.com/herodevs/cli/v2.0.0-beta.15/scripts/install.sh | bash
51
51
  ```
52
52
 
53
53
  ## Scanning Behavior
@@ -72,7 +72,7 @@ $ npm install -g @herodevs/cli@beta
72
72
  $ hd COMMAND
73
73
  running command...
74
74
  $ hd (--version)
75
- @herodevs/cli/2.0.0-beta.12 darwin-arm64 node-v24.10.0
75
+ @herodevs/cli/2.0.0-beta.15 darwin-arm64 node-v24.10.0
76
76
  $ hd --help [COMMAND]
77
77
  USAGE
78
78
  $ hd COMMAND
@@ -81,11 +81,58 @@ USAGE
81
81
  <!-- usagestop -->
82
82
  ## Commands
83
83
  <!-- commands -->
84
+ * [`hd auth login`](#hd-auth-login)
85
+ * [`hd auth logout`](#hd-auth-logout)
86
+ * [`hd auth provision-ci-token`](#hd-auth-provision-ci-token)
84
87
  * [`hd help [COMMAND]`](#hd-help-command)
85
88
  * [`hd report committers`](#hd-report-committers)
86
89
  * [`hd scan eol`](#hd-scan-eol)
90
+ * [`hd tracker init`](#hd-tracker-init)
91
+ * [`hd tracker run`](#hd-tracker-run)
87
92
  * [`hd update [CHANNEL]`](#hd-update-channel)
88
- * **NOTE:** Only applies to [binary installation method](#binary-installation). NPM users should use [`npm install`](#global-npm-installation) to update to the latest version.
93
+ * **NOTE:** Only applies to [binary installation method](#binary-installation). NPM users should use [`npm install`](#global-npm-installation) to update to the latest version.
94
+
95
+ ## `hd auth login`
96
+
97
+ OAuth CLI login
98
+
99
+ ```
100
+ USAGE
101
+ $ hd auth login
102
+
103
+ DESCRIPTION
104
+ OAuth CLI login
105
+ ```
106
+
107
+ _See code: [src/commands/auth/login.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.15/src/commands/auth/login.ts)_
108
+
109
+ ## `hd auth logout`
110
+
111
+ Logs out of HeroDevs OAuth and clears stored tokens
112
+
113
+ ```
114
+ USAGE
115
+ $ hd auth logout
116
+
117
+ DESCRIPTION
118
+ Logs out of HeroDevs OAuth and clears stored tokens
119
+ ```
120
+
121
+ _See code: [src/commands/auth/logout.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.15/src/commands/auth/logout.ts)_
122
+
123
+ ## `hd auth provision-ci-token`
124
+
125
+ Provision a CI/CD long-lived refresh token for headless auth
126
+
127
+ ```
128
+ USAGE
129
+ $ hd auth provision-ci-token
130
+
131
+ DESCRIPTION
132
+ Provision a CI/CD long-lived refresh token for headless auth
133
+ ```
134
+
135
+ _See code: [src/commands/auth/provision-ci-token.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.15/src/commands/auth/provision-ci-token.ts)_
89
136
 
90
137
  ## `hd help [COMMAND]`
91
138
 
@@ -105,7 +152,7 @@ DESCRIPTION
105
152
  Display help for hd.
106
153
  ```
107
154
 
108
- _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.34/src/commands/help.ts)_
155
+ _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/6.2.37/src/commands/help.ts)_
109
156
 
110
157
  ## `hd report committers`
111
158
 
@@ -113,15 +160,20 @@ Generate report of committers to a git repository
113
160
 
114
161
  ```
115
162
  USAGE
116
- $ hd report committers [--json] [-m <value>] [-c] [-s]
163
+ $ hd report committers [--json] [-x <value>...] [-d <value>] [--monthly] [-m <value> | -s <value> | -e <value> | | ]
164
+ [-c] [-s]
117
165
 
118
166
  FLAGS
119
- -c, --csv Output in CSV format
120
- -m, --months=<value> [default: 12] The number of months of git history to review
121
- -s, --save Save the committers report as herodevs.committers.<output>
122
-
123
- GLOBAL FLAGS
124
- --json Format output as json.
167
+ -c, --csv Output in CSV format
168
+ -d, --directory=<value> Directory to search
169
+ -e, --afterDate=<value> [default: 2025-02-26] Start date (format: yyyy-MM-dd)
170
+ -m, --months=<value> [default: 12] The number of months of git history to review. Cannot be used along beforeDate
171
+ and afterDate
172
+ -s, --beforeDate=<value> [default: 2026-02-26] End date (format: yyyy-MM-dd)
173
+ -s, --save Save the committers report as herodevs.committers.<output>
174
+ -x, --exclude=<value>... Path Exclusions (eg -x="./src/bin" -x="./dist")
175
+ --json Output to JSON format
176
+ --monthly Break down by calendar month.
125
177
 
126
178
  DESCRIPTION
127
179
  Generate report of committers to a git repository
@@ -136,7 +188,7 @@ EXAMPLES
136
188
  $ hd report committers --csv
137
189
  ```
138
190
 
139
- _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.12/src/commands/report/committers.ts)_
191
+ _See code: [src/commands/report/committers.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.15/src/commands/report/committers.ts)_
140
192
 
141
193
  ## `hd scan eol`
142
194
 
@@ -145,7 +197,7 @@ Scan a given SBOM for EOL data
145
197
  ```
146
198
  USAGE
147
199
  $ hd scan eol [--json] [-f <value> | -d <value>] [-s] [-o <value>] [--saveSbom] [--sbomOutput <value>]
148
- [--saveTrimmedSbom] [--hideReportUrl] [--version]
200
+ [--saveTrimmedSbom] [--hideReportUrl] [--automated] [--version]
149
201
 
150
202
  FLAGS
151
203
  -d, --dir=<value> [default: <current directory>] The directory to scan in order to create a cyclonedx SBOM
@@ -153,6 +205,7 @@ FLAGS
153
205
  -o, --output=<value> Save the generated report to a custom path (defaults to herodevs.report.json when not
154
206
  provided)
155
207
  -s, --save Save the generated report as herodevs.report.json in the scanned directory
208
+ --automated Mark scan as automated (for CI/CD pipelines)
156
209
  --hideReportUrl Hide the generated web report URL for this scan
157
210
  --saveSbom Save the generated SBOM as herodevs.sbom.json in the scanned directory
158
211
  --saveTrimmedSbom Save the trimmed SBOM as herodevs.sbom-trimmed.json in the scanned directory
@@ -182,16 +235,71 @@ EXAMPLES
182
235
 
183
236
  $ hd scan eol --save --saveSbom
184
237
 
185
- Save the report and SBOM to custom paths
186
-
187
- $ hd scan eol --dir . --save --saveSbom --output ./reports/my-report.json --sbomOutput ./reports/my-sbom.json
188
-
189
238
  Output the report in JSON format (for APIs, CI, etc.)
190
239
 
191
240
  $ hd scan eol --json
192
241
  ```
193
242
 
194
- _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.12/src/commands/scan/eol.ts)_
243
+ _See code: [src/commands/scan/eol.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.15/src/commands/scan/eol.ts)_
244
+
245
+ ## `hd tracker init`
246
+
247
+ Initialize the tracker configuration
248
+
249
+ ```
250
+ USAGE
251
+ $ hd tracker init [--force -o] [-d <value>] [-f <value>] [-i <value>...]
252
+
253
+ FLAGS
254
+ -d, --outputDir=<value> [default: hd-tracker] Output directory for the tracker configuration file
255
+ -f, --configFile=<value> [default: config.json] Filename for the tracker configuration file
256
+ -i, --ignorePatterns=<value>... [default: node_modules] Ignore patterns to use for the tracker configuration file
257
+ -o, --overwrite Overwrites the tracker configuration file if it exists
258
+ --force Force tracker configuration file creation. Use with --overwrite flag
259
+
260
+ DESCRIPTION
261
+ Initialize the tracker configuration
262
+
263
+ EXAMPLES
264
+ $ hd tracker init
265
+
266
+ $ hd tracker init -d trackerDir
267
+
268
+ $ hd tracker init -d trackerDir -f configFileName
269
+
270
+ $ hd tracker init -i node_modules
271
+
272
+ $ hd tracker init -i node_modules -i custom_modules
273
+
274
+ $ hd tracker init -o
275
+ ```
276
+
277
+ _See code: [src/commands/tracker/init.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.15/src/commands/tracker/init.ts)_
278
+
279
+ ## `hd tracker run`
280
+
281
+ Run the tracker
282
+
283
+ ```
284
+ USAGE
285
+ $ hd tracker run [-d <value>] [-f <value>]
286
+
287
+ FLAGS
288
+ -d, --configDir=<value> [default: hd-tracker] Directory where the tracker configuration file resides
289
+ -f, --configFile=<value> [default: config.json] Filename for the tracker configuration file
290
+
291
+ DESCRIPTION
292
+ Run the tracker
293
+
294
+ EXAMPLES
295
+ $ hd tracker run
296
+
297
+ $ hd tracker run -d tracker-configuration
298
+
299
+ $ hd tracker run -d tracker -f settings.json
300
+ ```
301
+
302
+ _See code: [src/commands/tracker/run.ts](https://github.com/herodevs/cli/blob/v2.0.0-beta.15/src/commands/tracker/run.ts)_
195
303
 
196
304
  ## `hd update [CHANNEL]`
197
305
 
@@ -231,13 +339,77 @@ EXAMPLES
231
339
  $ hd update --available
232
340
  ```
233
341
 
234
- _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.7.13/src/commands/update.ts)_
342
+ _See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/4.7.18/src/commands/update.ts)_
235
343
  <!-- commandsstop -->
236
344
 
237
345
  ## CI/CD Usage
238
346
 
239
347
  You can use `@herodevs/cli` in your CI/CD pipelines to automate EOL scanning.
240
348
 
349
+ ### CI/CD authentication
350
+
351
+ For headless use in CI/CD (e.g. GitHub Actions, GitLab CI), the CLI supports long-lived organization-scoped refresh tokens. You do not need to run an interactive login in the pipeline.
352
+
353
+ **One-time setup (interactive):**
354
+
355
+ ```bash
356
+ hd auth login
357
+ hd auth provision-ci-token
358
+ ```
359
+
360
+ Copy the token output, add as CI secret: `HD_CI_CREDENTIAL`
361
+
362
+ **CI pipeline (headless):** Run `hd scan eol` directly with `HD_CI_CREDENTIAL` set. The CLI exchanges the token for an access token automatically:
363
+
364
+ ```bash
365
+ export HD_CI_CREDENTIAL="<token>"
366
+ hd scan eol --dir .
367
+ ```
368
+
369
+ | Secret / Env Var | Purpose |
370
+ |------------------|---------|
371
+ | `HD_CI_CREDENTIAL` | Refresh token from provision; exchanged for access token |
372
+
373
+ #### Local testing
374
+
375
+ Reproduce the CI flow locally:
376
+
377
+ ```bash
378
+ export HD_CI_CREDENTIAL="<token-from-provision>"
379
+ hd scan eol --dir /path/to/project
380
+ ```
381
+
382
+ #### GitHub Actions (authenticated scan)
383
+
384
+ Add secret `HD_CI_CREDENTIAL` in your repository or organization, then:
385
+
386
+ ```yaml
387
+ - uses: actions/checkout@v5
388
+ - uses: actions/setup-node@v6
389
+ with:
390
+ node-version: '24'
391
+ - name: Run EOL Scan
392
+ env:
393
+ HD_CI_CREDENTIAL: ${{ secrets.HD_CI_CREDENTIAL }}
394
+ run: npx @herodevs/cli@beta scan eol -s
395
+ ```
396
+
397
+ #### GitLab CI (authenticated scan)
398
+
399
+ Add CI/CD variable `HD_CI_CREDENTIAL` (masked) in your project:
400
+
401
+ ```yaml
402
+ eol-scan:
403
+ image: node:24
404
+ variables:
405
+ HD_CI_CREDENTIAL: $HD_CI_CREDENTIAL
406
+ script:
407
+ - npx @herodevs/cli@beta scan eol -s
408
+ artifacts:
409
+ paths:
410
+ - herodevs.report.json
411
+ ```
412
+
241
413
  ### Using the Docker Image (Recommended)
242
414
 
243
415
  We provide a Docker image that's pre-configured to run EOL scans. Based on [`cdxgen`](https://github.com/CycloneDX/cdxgen),
@@ -0,0 +1,3 @@
1
+ import { ApolloClient } from '@apollo/client/core';
2
+ export type TokenProvider = (forceRefresh?: boolean) => Promise<string>;
3
+ export declare const createApollo: (uri: string, tokenProvider?: TokenProvider) => ApolloClient;
@@ -0,0 +1,53 @@
1
+ import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client/core';
2
+ import { requireAccessTokenForScan } from "../service/auth.svc.js";
3
+ function isTokenEndpoint(input) {
4
+ let urlString;
5
+ if (typeof input === 'string') {
6
+ urlString = input;
7
+ }
8
+ else if (input instanceof Request) {
9
+ urlString = input.url;
10
+ }
11
+ else {
12
+ urlString = input.toString();
13
+ }
14
+ try {
15
+ const url = new URL(urlString);
16
+ return url.pathname.endsWith('/token');
17
+ }
18
+ catch {
19
+ const pathOnly = urlString.split('?')[0].split('#')[0];
20
+ return pathOnly.endsWith('/token');
21
+ }
22
+ }
23
+ const createAuthorizedFetch = (tokenProvider) => async (input, init) => {
24
+ const headers = new Headers(init?.headers);
25
+ const token = await tokenProvider();
26
+ if (token) {
27
+ headers.set('Authorization', `Bearer ${token}`);
28
+ }
29
+ const response = await fetch(input, { ...init, headers });
30
+ if (response.status === 401 &&
31
+ !isTokenEndpoint(input) &&
32
+ (init?.method === 'GET' || init?.method === undefined || init?.method === 'POST')) {
33
+ const refreshed = await tokenProvider(true);
34
+ const retryHeaders = new Headers(init?.headers);
35
+ retryHeaders.set('Authorization', `Bearer ${refreshed}`);
36
+ return fetch(input, { ...init, headers: retryHeaders });
37
+ }
38
+ return response;
39
+ };
40
+ export const createApollo = (uri, tokenProvider = requireAccessTokenForScan) => new ApolloClient({
41
+ cache: new InMemoryCache(),
42
+ defaultOptions: {
43
+ query: { fetchPolicy: 'no-cache', errorPolicy: 'all' },
44
+ mutate: { errorPolicy: 'all' },
45
+ },
46
+ link: new HttpLink({
47
+ uri,
48
+ fetch: createAuthorizedFetch(tokenProvider),
49
+ headers: {
50
+ 'User-Agent': `hdcli/${process.env.npm_package_version ?? 'unknown'}`,
51
+ },
52
+ }),
53
+ });
@@ -0,0 +1,26 @@
1
+ export type IamAccessOrgTokensInput = {
2
+ orgId: number;
3
+ previousToken?: never;
4
+ } | {
5
+ orgId?: never;
6
+ previousToken: string;
7
+ };
8
+ export interface ProvisionCITokenResponse {
9
+ refresh_token: string;
10
+ }
11
+ export interface ProvisionCITokenOptions {
12
+ orgId?: number;
13
+ previousToken?: string | null;
14
+ }
15
+ export declare function getOrgAccessTokensUnauthenticated(input: IamAccessOrgTokensInput): Promise<{
16
+ accessToken: string;
17
+ refreshToken: string;
18
+ }>;
19
+ export interface ExchangeCITokenOptions {
20
+ refreshToken: string;
21
+ }
22
+ export declare function exchangeCITokenForAccess(options: ExchangeCITokenOptions): Promise<{
23
+ accessToken: string;
24
+ refreshToken: string;
25
+ }>;
26
+ export declare function provisionCIToken(options?: ProvisionCITokenOptions): Promise<ProvisionCITokenResponse>;
@@ -0,0 +1,95 @@
1
+ import { config } from "../config/constants.js";
2
+ import { requireAccessToken } from "../service/auth.svc.js";
3
+ import { debugLogger } from "../service/log.svc.js";
4
+ import { createApollo } from "./apollo.client.js";
5
+ import { ApiError, isApiErrorCode } from "./errors.js";
6
+ import { getOrgAccessTokensMutation } from "./gql-operations.js";
7
+ import { getGraphQLErrors } from "./graphql-errors.js";
8
+ const graphqlUrl = `${config.graphqlHost}${config.graphqlPath}`;
9
+ const noAuthTokenProvider = async () => '';
10
+ function extractErrorCode(errors) {
11
+ const code = errors[0]?.extensions?.code;
12
+ if (!code || !isApiErrorCode(code))
13
+ return;
14
+ return code;
15
+ }
16
+ async function getOrgAccessTokens(input) {
17
+ const client = createApollo(graphqlUrl, requireAccessToken);
18
+ const res = await client.mutate({
19
+ mutation: getOrgAccessTokensMutation,
20
+ variables: {
21
+ input,
22
+ },
23
+ });
24
+ const errors = getGraphQLErrors(res);
25
+ if (res?.error || errors?.length) {
26
+ debugLogger('Error returned from getOrgAccessTokens mutation: %o', res.error ?? errors);
27
+ if (errors?.length) {
28
+ const code = extractErrorCode(errors);
29
+ if (code) {
30
+ throw new ApiError(errors[0].message ?? 'CI token provisioning failed', code);
31
+ }
32
+ throw new Error(errors[0].message ?? 'CI token provisioning failed');
33
+ }
34
+ const msg = res?.error instanceof Error ? res.error.message : res?.error ? String(res.error) : '';
35
+ throw new Error(msg || 'CI token provisioning failed');
36
+ }
37
+ const tokens = res.data?.iamV2?.access?.getOrgAccessTokens;
38
+ if (!tokens?.refreshToken || tokens.refreshToken.trim() === '') {
39
+ throw new Error('CI token provisioning response missing refreshToken');
40
+ }
41
+ return {
42
+ accessToken: tokens.accessToken ?? '',
43
+ refreshToken: tokens.refreshToken,
44
+ };
45
+ }
46
+ export async function getOrgAccessTokensUnauthenticated(input) {
47
+ return callGetOrgAccessTokensInternal(input, noAuthTokenProvider);
48
+ }
49
+ async function callGetOrgAccessTokensInternal(input, tokenProvider) {
50
+ const client = createApollo(graphqlUrl, tokenProvider);
51
+ const res = await client.mutate({
52
+ mutation: getOrgAccessTokensMutation,
53
+ variables: { input },
54
+ });
55
+ const errors = getGraphQLErrors(res);
56
+ if (res?.error || errors?.length) {
57
+ debugLogger('Error returned from getOrgAccessTokens mutation: %o', res.error ?? errors);
58
+ if (errors?.length) {
59
+ const code = extractErrorCode(errors);
60
+ if (code) {
61
+ throw new ApiError(errors[0].message ?? 'CI token refresh failed', code);
62
+ }
63
+ throw new Error(errors[0].message ?? 'CI token refresh failed');
64
+ }
65
+ const msg = res?.error instanceof Error ? res.error.message : res?.error ? String(res.error) : '';
66
+ throw new Error(msg || 'CI token refresh failed');
67
+ }
68
+ const tokens = res.data?.iamV2?.access?.getOrgAccessTokens;
69
+ if (!tokens?.accessToken) {
70
+ throw new Error('getOrgAccessTokens response missing accessToken');
71
+ }
72
+ return {
73
+ accessToken: tokens.accessToken,
74
+ refreshToken: tokens.refreshToken ?? '',
75
+ };
76
+ }
77
+ export async function exchangeCITokenForAccess(options) {
78
+ const { refreshToken } = options;
79
+ return callGetOrgAccessTokensInternal({ previousToken: refreshToken }, noAuthTokenProvider);
80
+ }
81
+ export async function provisionCIToken(options = {}) {
82
+ const { orgId, previousToken } = options;
83
+ let input;
84
+ if (previousToken != null && previousToken !== '') {
85
+ input = { previousToken };
86
+ }
87
+ else if (orgId != null) {
88
+ input = { orgId };
89
+ }
90
+ else {
91
+ throw new Error('Either orgId or previousToken is required to provision a CI token');
92
+ }
93
+ const result = await getOrgAccessTokens(input);
94
+ return { refresh_token: result.refreshToken };
95
+ }
@@ -0,0 +1,8 @@
1
+ declare const API_ERROR_CODES: readonly ["SESSION_EXPIRED", "INVALID_TOKEN", "UNAUTHENTICATED", "FORBIDDEN"];
2
+ export type ApiErrorCode = (typeof API_ERROR_CODES)[number];
3
+ export declare class ApiError extends Error {
4
+ readonly code: ApiErrorCode;
5
+ constructor(message: string, code: ApiErrorCode);
6
+ }
7
+ export declare function isApiErrorCode(code: string): code is ApiErrorCode;
8
+ export {};
@@ -0,0 +1,13 @@
1
+ const API_ERROR_CODES = ['SESSION_EXPIRED', 'INVALID_TOKEN', 'UNAUTHENTICATED', 'FORBIDDEN'];
2
+ const VALID_API_ERROR_CODES = new Set(API_ERROR_CODES);
3
+ export class ApiError extends Error {
4
+ code;
5
+ constructor(message, code) {
6
+ super(message);
7
+ this.name = 'ApiError';
8
+ this.code = code;
9
+ }
10
+ }
11
+ export function isApiErrorCode(code) {
12
+ return VALID_API_ERROR_CODES.has(code);
13
+ }
@@ -1,2 +1,5 @@
1
1
  export declare const createReportMutation: import("graphql/language/ast.js").DocumentNode;
2
2
  export declare const getEolReportQuery: import("graphql/language/ast.js").DocumentNode;
3
+ export declare const userSetupStatusQuery: import("graphql/language/ast.js").DocumentNode;
4
+ export declare const completeUserSetupMutation: import("graphql/language/ast.js").DocumentNode;
5
+ export declare const getOrgAccessTokensMutation: import("graphql/language/ast.js").DocumentNode;
@@ -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,29 +1,32 @@
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";
4
3
  import { stripTypename } from "../utils/strip-typename.js";
4
+ import { createApollo } from "./apollo.client.js";
5
+ import { ApiError, isApiErrorCode } from "./errors.js";
5
6
  import { createReportMutation, getEolReportQuery } from "./gql-operations.js";
6
- export const createApollo = (uri) => new ApolloClient({
7
- cache: new InMemoryCache(),
8
- defaultOptions: {
9
- query: { fetchPolicy: 'no-cache', errorPolicy: 'all' },
10
- mutate: { errorPolicy: 'all' },
11
- },
12
- link: new HttpLink({
13
- uri,
14
- headers: {
15
- 'User-Agent': `hdcli/${process.env.npm_package_version ?? 'unknown'}`,
16
- },
17
- }),
18
- });
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
+ }
19
14
  export const SbomScanner = (client) => {
20
15
  return async (input) => {
21
- const res = await client.mutate({
16
+ let res;
17
+ res = await client.mutate({
22
18
  mutation: createReportMutation,
23
19
  variables: { input },
24
20
  });
25
- if (res?.errors?.length) {
26
- debugLogger('GraphQL errors in createReport: %o', res.errors);
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
+ }
27
30
  throw new Error('Failed to create EOL report');
28
31
  }
29
32
  const result = res.data?.eol?.createReport;
@@ -47,10 +50,18 @@ export const SbomScanner = (client) => {
47
50
  let reportMetadata = null;
48
51
  for (let i = 0; i < pages.length; i += config.concurrentPageRequests) {
49
52
  const batch = pages.slice(i, i + config.concurrentPageRequests);
50
- const batchResponses = await Promise.all(batch);
53
+ let batchResponses;
54
+ batchResponses = await Promise.all(batch);
51
55
  for (const response of batchResponses) {
52
- if (response?.errors?.length) {
53
- debugLogger('GraphQL errors in getReport query: %o', response.errors);
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
+ }
54
65
  throw new Error('Failed to fetch EOL report');
55
66
  }
56
67
  const report = response.data.eol.report;