Skip to content

Commit af76ae6

Browse files
feat: implement functionality to fetch scorecard using api key
1 parent 68c1baa commit af76ae6

File tree

5 files changed

+56
-96
lines changed

5 files changed

+56
-96
lines changed

packages/cli/src/commands/scorecard-classic/auth/login-handler.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { logger } from '@redocly/openapi-core';
2-
import { blue } from 'colorette';
31
import { RedoclyOAuthClient } from '../../../auth/oauth-client.js';
42
import { getReuniteUrl } from '../../../reunite/api/index.js';
53
import { exitWithError } from '../../../utils/error.js';
@@ -8,13 +6,11 @@ import type { Config } from '@redocly/openapi-core';
86

97
export async function handleLoginAndFetchToken(config: Config): Promise<string | undefined> {
108
const reuniteUrl = getReuniteUrl(config, config.resolvedConfig?.residency);
9+
1110
const oauthClient = new RedoclyOAuthClient();
1211
let accessToken = await oauthClient.getAccessToken(reuniteUrl);
1312

1413
if (!accessToken) {
15-
logger.info(`\n${blue('Authentication required to fetch remote scorecard configuration.')}\n`);
16-
logger.info(`Please login to continue:\n`);
17-
1814
try {
1915
await oauthClient.login(reuniteUrl);
2016
accessToken = await oauthClient.getAccessToken(reuniteUrl);

packages/cli/src/commands/scorecard-classic/index.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,23 @@ export async function handleScorecardClassic({ argv, config }: CommandArgs<Score
1414
const externalRefResolver = new BaseResolver(config.resolve);
1515
const { bundle: document } = await bundle({ config, ref: path });
1616
const projectUrl = argv['project-url'] || config.resolvedConfig.scorecard?.fromProjectUrl;
17+
const apiKey = process.env.REDOCLY_AUTHORIZATION;
1718

1819
if (!projectUrl) {
1920
exitWithError(
20-
'scorecard.fromProjectUrl is not configured. Please provide it via --project-url flag or configure it in redocly.yaml. Learn more: https://redocly.com/docs/realm/config/scorecard#fromprojecturl-example'
21+
'Scorecard is not configured. Please provide it via --project-url flag or configure it in redocly.yaml. Learn more: https://redocly.com/docs/realm/config/scorecard#fromprojecturl-example'
2122
);
2223
}
2324

24-
const accessToken = await handleLoginAndFetchToken(config);
25+
const auth = apiKey || (await handleLoginAndFetchToken(config));
2526

26-
if (!accessToken) {
27-
exitWithError('Failed to obtain access token.');
27+
if (!auth) {
28+
exitWithError('Failed to obtain access token or API key.');
2829
}
2930

30-
const remoteScorecardAndPlugins = await fetchRemoteScorecardAndPlugins(projectUrl, accessToken);
31-
32-
const scorecard =
33-
remoteScorecardAndPlugins?.scorecard ||
34-
config.resolvedConfig.scorecardClassic ||
35-
config.resolvedConfig.scorecard;
31+
const remoteScorecardAndPlugins = await fetchRemoteScorecardAndPlugins(projectUrl, auth);
3632

33+
const scorecard = remoteScorecardAndPlugins?.scorecard;
3734
if (!scorecard) {
3835
exitWithError(
3936
'No scorecard configuration found. Please configure scorecard in your redocly.yaml or ensure remote scorecard is accessible.'
Lines changed: 45 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,39 @@
1-
import { logger } from '@redocly/openapi-core';
1+
import { exitWithError } from 'cli/src/utils/error.js';
22

3-
import type { RemoteScorecardAndPlugins, Organization, Project, PaginatedList } from '../types.js';
3+
import type { RemoteScorecardAndPlugins, Project } from '../types.js';
44

55
export async function fetchRemoteScorecardAndPlugins(
66
projectUrl: string,
7-
accessToken: string
7+
auth: string
88
): Promise<RemoteScorecardAndPlugins | undefined> {
99
const parsedProjectUrl = parseProjectUrl(projectUrl);
1010

1111
if (!parsedProjectUrl) {
12-
logger.warn(`Invalid project URL format: ${projectUrl}`);
13-
return;
12+
exitWithError(`Invalid project URL format: ${projectUrl}`);
1413
}
1514

1615
const { residency, orgSlug, projectSlug } = parsedProjectUrl;
17-
18-
const organization = await fetchOrganizationBySlug(residency, orgSlug, accessToken);
19-
20-
if (!organization) {
21-
logger.warn(`Organization not found: ${orgSlug}`);
22-
return;
23-
}
24-
25-
const project = await fetchProjectBySlug(residency, organization.id, projectSlug, accessToken);
26-
27-
if (!project) {
28-
logger.warn(`Project not found: ${projectSlug}`);
29-
return;
30-
}
31-
32-
const scorecard = project?.config.scorecard;
33-
34-
if (!scorecard) {
35-
logger.warn('No scorecard configuration found in the remote project.');
36-
return;
16+
const apiKey = process.env.REDOCLY_AUTHORIZATION;
17+
18+
try {
19+
const project = await fetchProjectConfigBySlugs(residency, orgSlug, projectSlug, apiKey, auth);
20+
const scorecard = project?.config.scorecard;
21+
22+
if (!scorecard) {
23+
throw new Error('No scorecard configuration found.');
24+
}
25+
26+
const plugins = project.config.pluginsUrl
27+
? await fetchPlugins(project.config.pluginsUrl)
28+
: undefined;
29+
30+
return {
31+
scorecard,
32+
plugins,
33+
};
34+
} catch (error) {
35+
exitWithError(error.message);
3736
}
38-
39-
const plugins = project.config.pluginsUrl
40-
? await fetchPlugins(project.config.pluginsUrl)
41-
: undefined;
42-
43-
return {
44-
scorecard,
45-
plugins,
46-
};
4737
}
4838

4939
function parseProjectUrl(
@@ -65,47 +55,29 @@ function parseProjectUrl(
6555
};
6656
}
6757

68-
async function fetchOrganizationBySlug(
58+
async function fetchProjectConfigBySlugs(
6959
residency: string,
7060
orgSlug: string,
71-
accessToken: string
72-
): Promise<Organization | undefined> {
73-
const orgsUrl = new URL(`${residency}/api/orgs`);
74-
orgsUrl.searchParams.set('filter', `slug:${orgSlug}`);
75-
orgsUrl.searchParams.set('limit', '1');
76-
77-
const authHeaders = createAuthHeaders(accessToken);
78-
const organizationResponse = await fetch(orgsUrl, { headers: authHeaders });
79-
80-
if (organizationResponse.status !== 200) {
81-
return;
82-
}
83-
84-
const organizations: PaginatedList<Organization> = await organizationResponse.json();
85-
86-
return organizations.items[0];
87-
}
88-
89-
async function fetchProjectBySlug(
90-
residency: string,
91-
orgId: string,
9261
projectSlug: string,
62+
apiKey: string | undefined,
9363
accessToken: string
9464
): Promise<Project | undefined> {
95-
const projectsUrl = new URL(`${residency}/api/orgs/${orgId}/projects`);
96-
projectsUrl.searchParams.set('filter', `slug:${projectSlug}`);
97-
projectsUrl.searchParams.set('limit', '1');
65+
const authHeaders = createAuthHeaders(apiKey, accessToken);
66+
const projectUrl = new URL(`${residency}/api/orgs/${orgSlug}/projects/${projectSlug}`);
9867

99-
const authHeaders = createAuthHeaders(accessToken);
100-
const projectsResponse = await fetch(projectsUrl, { headers: authHeaders });
68+
const projectResponse = await fetch(projectUrl, { headers: authHeaders });
10169

102-
if (projectsResponse.status !== 200) {
103-
return;
70+
if (projectResponse.status === 401 || projectResponse.status === 403) {
71+
throw new Error(
72+
`Unauthorized access to project: ${projectSlug}. Please check your credentials.`
73+
);
10474
}
10575

106-
const projects: PaginatedList<Project> = await projectsResponse.json();
76+
if (projectResponse.status !== 200) {
77+
throw new Error(`Failed to fetch project: ${projectSlug}. Status: ${projectResponse.status}`);
78+
}
10779

108-
return projects.items[0];
80+
return projectResponse.json();
10981
}
11082

11183
async function fetchPlugins(pluginsUrl: string): Promise<string | undefined> {
@@ -118,6 +90,13 @@ async function fetchPlugins(pluginsUrl: string): Promise<string | undefined> {
11890
return pluginsResponse.text();
11991
}
12092

121-
function createAuthHeaders(accessToken: string) {
93+
function createAuthHeaders(
94+
apiKey: string | undefined,
95+
accessToken: string
96+
): Record<string, string> {
97+
if (apiKey) {
98+
return { Authorization: `Bearer ${apiKey}` };
99+
}
100+
122101
return { Cookie: `accessToken=${accessToken}` };
123102
}

packages/cli/src/commands/scorecard-classic/types.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,8 @@ export type RemoteScorecardAndPlugins = {
1414
plugins: string | undefined;
1515
};
1616

17-
export type Organization = {
18-
id: `org_${string}`;
19-
slug: string;
20-
};
21-
2217
export type Project = {
2318
id: `prj_${string}`;
2419
slug: string;
2520
config: ResolvedConfig & { pluginsUrl?: string; scorecardClassic?: ResolvedConfig['scorecard'] };
2621
};
27-
28-
export type PaginatedList<T> = {
29-
items: T[];
30-
};

packages/cli/src/commands/scorecard-classic/validation/plugin-evaluator.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,9 @@ export async function evaluatePluginsFromCode(pluginsCode?: string): Promise<Plu
1212
}
1313

1414
try {
15-
const normalizedDirname =
16-
typeof __dirname === 'undefined' ? '' : __dirname.replaceAll(/\\/g, '/');
17-
const pluginsCodeWithDirname = pluginsCode.replaceAll(
18-
'__redocly_dirname',
19-
`"${normalizedDirname}"`
20-
);
15+
const dirname = import.meta.url;
16+
const pluginsCodeWithDirname = pluginsCode.replaceAll('__redocly_dirname', `"${dirname}"`);
17+
2118
const base64 = btoa(pluginsCodeWithDirname);
2219
const dataUri = `data:text/javascript;base64,${base64}`;
2320
const module: PluginsModule = await import(dataUri);

0 commit comments

Comments
 (0)