Skip to content

Commit 2fa5f27

Browse files
committed
chore: fix pr suggestions and code cleanups
1 parent da30ac7 commit 2fa5f27

4 files changed

Lines changed: 49 additions & 27 deletions

File tree

.envrc.example

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ export ANALYTICS_URL='https://eol-api.herodevs.com/track';
99
export OAUTH_CONNECT_URL='';
1010
export OAUTH_CLIENT_ID='';
1111

12+
# IAM (for CI token provisioning)
13+
# export IAM_HOST='https://apps.herodevs.io/api/iam';
14+
# export IAM_PATH='/graphql';
15+
16+
# Auth / User setup (optional)
17+
# export ENABLE_AUTH='true';
18+
# export ENABLE_USER_SETUP='true';
19+
20+
# CI token (for headless auth - set when testing CI flow locally)
21+
# export HD_ORG_ID='1234';
22+
# export HD_AUTH_TOKEN='<long-lived-refresh-token>';
23+
# export HD_ACCESS_TOKEN='<access-token-from-auth-ci-login>';
24+
1225
# Performance tuning (optional)
1326
# export CONCURRENT_PAGE_REQUESTS='3';
1427
# export PAGE_SIZE='500';

src/api/apollo.client.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,22 @@ import { requireAccessTokenForScan } from '../service/auth.svc.ts';
55
export type TokenProvider = (forceRefresh?: boolean) => Promise<string>;
66

77
function isTokenEndpoint(input: string | URL | Request): boolean {
8-
const url = typeof input === 'string' ? input : input instanceof Request ? input.url : input.toString();
9-
return url.endsWith('/token');
8+
let urlString: string;
9+
if (typeof input === 'string') {
10+
urlString = input;
11+
} else if (input instanceof Request) {
12+
urlString = input.url;
13+
} else {
14+
urlString = input.toString();
15+
}
16+
17+
try {
18+
const url = new URL(urlString);
19+
return url.pathname.endsWith('/token');
20+
} catch {
21+
const pathOnly = urlString.split('?')[0].split('#')[0];
22+
return pathOnly.endsWith('/token');
23+
}
1024
}
1125

1226
const createAuthorizedFetch =
@@ -21,7 +35,7 @@ const createAuthorizedFetch =
2135
}
2236
}
2337

24-
let response = await fetch(input, { ...init, headers });
38+
const response = await fetch(input, { ...init, headers });
2539

2640
if (
2741
config.enableAuth &&
@@ -32,7 +46,7 @@ const createAuthorizedFetch =
3246
const refreshed = await tokenProvider(true);
3347
const retryHeaders = new Headers(init?.headers);
3448
retryHeaders.set('Authorization', `Bearer ${refreshed}`);
35-
response = await fetch(input, { ...init, headers: retryHeaders });
49+
return fetch(input, { ...init, headers: retryHeaders });
3650
}
3751

3852
return response;

src/api/ci-token.client.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { ApiError, type ApiErrorCode, isApiErrorCode } from './errors.ts';
88
import { getOrgAccessTokensMutation } from './gql-operations.ts';
99
import { getGraphQLErrors } from './graphql-errors.ts';
1010

11+
const graphqlUrl = `${config.iamHost}${config.iamPath}`;
12+
1113
const noAuthTokenProvider = async (): Promise<string> => '';
1214

1315
function createOptionalTokenProvider(token?: string) {
@@ -41,20 +43,16 @@ type GetOrgAccessTokensResponse = {
4143
};
4244
};
4345

44-
function getGraphqlUrl(): string {
45-
return `${config.iamHost}${config.iamPath}`;
46-
}
47-
4846
function extractErrorCode(errors: ReadonlyArray<GraphQLFormattedError>): ApiErrorCode | undefined {
4947
const code = (errors[0]?.extensions as { code?: string })?.code;
50-
if (!code || !isApiErrorCode(code)) return undefined;
48+
if (!code || !isApiErrorCode(code)) return;
5149
return code;
5250
}
5351

5452
async function getOrgAccessTokens(
5553
input: IamAccessOrgTokensInput,
5654
): Promise<{ accessToken: string; refreshToken: string }> {
57-
const client = createApollo(getGraphqlUrl(), requireAccessToken);
55+
const client = createApollo(graphqlUrl, requireAccessToken);
5856
const res = await client.mutate<GetOrgAccessTokensResponse, { input: IamAccessOrgTokensInput }>({
5957
mutation: getOrgAccessTokensMutation,
6058
variables: {
@@ -87,7 +85,7 @@ async function getOrgAccessTokens(
8785
};
8886
}
8987

90-
async function getOrgAccessTokensUnauthenticated(
88+
export async function getOrgAccessTokensUnauthenticated(
9189
input: IamAccessOrgTokensInput,
9290
): Promise<{ accessToken: string; refreshToken: string }> {
9391
return callGetOrgAccessTokensInternal(input, noAuthTokenProvider);
@@ -99,7 +97,7 @@ async function callGetOrgAccessTokensInternal(
9997
input: IamAccessOrgTokensInput,
10098
tokenProvider: TokenProvider,
10199
): Promise<{ accessToken: string; refreshToken: string }> {
102-
const client = createApollo(getGraphqlUrl(), tokenProvider);
100+
const client = createApollo(graphqlUrl, tokenProvider);
103101
const res = await client.mutate<GetOrgAccessTokensResponse, { input: IamAccessOrgTokensInput }>({
104102
mutation: getOrgAccessTokensMutation,
105103
variables: { input },
@@ -144,16 +142,6 @@ export async function exchangeCITokenForAccess(
144142
return callGetOrgAccessTokensInternal({ orgId, previousToken: refreshToken }, tokenProvider);
145143
}
146144

147-
export async function getAccessTokenFromCIRefresh(
148-
refreshToken: string,
149-
orgId: number,
150-
): Promise<{ accessToken: string; refreshToken: string }> {
151-
return getOrgAccessTokensUnauthenticated({
152-
orgId,
153-
previousToken: refreshToken,
154-
});
155-
}
156-
157145
export async function provisionCIToken(options: ProvisionCITokenOptions = {}): Promise<ProvisionCITokenResponse> {
158146
const { orgId = null, previousToken = null } = options;
159147
const result = await getOrgAccessTokens({

test/api/ci-token.client.test.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ vi.mock('../../src/config/constants.ts', async (importOriginal) => {
1919

2020
import {
2121
exchangeCITokenForAccess,
22-
getAccessTokenFromCIRefresh,
22+
getOrgAccessTokensUnauthenticated,
2323
provisionCIToken,
2424
} from '../../src/api/ci-token.client.ts';
2525
import { FetchMock } from '../utils/mocks/fetch.mock.ts';
@@ -184,7 +184,7 @@ describe('ci-token.client', () => {
184184
await expect(provisionCIToken()).rejects.toThrow(/missing refreshToken/);
185185
});
186186

187-
describe('getAccessTokenFromCIRefresh', () => {
187+
describe('getOrgAccessTokensUnauthenticated', () => {
188188
it('calls IAM with orgId and previousToken, without Bearer header', async () => {
189189
fetchMock.push(
190190
mockGraphQLResponse({
@@ -199,7 +199,10 @@ describe('ci-token.client', () => {
199199
}),
200200
);
201201

202-
const result = await getAccessTokenFromCIRefresh('stored-ci-refresh-token', 42);
202+
const result = await getOrgAccessTokensUnauthenticated({
203+
orgId: 42,
204+
previousToken: 'stored-ci-refresh-token',
205+
});
203206
expect(result.accessToken).toBe('new-access-from-refresh');
204207
expect(result.refreshToken).toBe('new-refresh');
205208

@@ -221,13 +224,17 @@ describe('ci-token.client', () => {
221224
it('throws when GraphQL returns errors', async () => {
222225
fetchMock.push(mockGraphQLErrorResponse('Invalid refresh token'));
223226

224-
await expect(getAccessTokenFromCIRefresh('bad-token', 1)).rejects.toThrow(/Invalid refresh token/);
227+
await expect(
228+
getOrgAccessTokensUnauthenticated({ orgId: 1, previousToken: 'bad-token' }),
229+
).rejects.toThrow(/Invalid refresh token/);
225230
});
226231

227232
it('throws when response is not ok', async () => {
228233
fetchMock.push(mockErrorResponse(500, 'Internal Server Error'));
229234

230-
await expect(getAccessTokenFromCIRefresh('token', 1)).rejects.toThrow(/500/);
235+
await expect(
236+
getOrgAccessTokensUnauthenticated({ orgId: 1, previousToken: 'token' }),
237+
).rejects.toThrow(/500/);
231238
});
232239
});
233240

0 commit comments

Comments
 (0)