Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/young-carrots-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@gitbook/integration-gitlab': minor
---

Add support for custom proxy
4 changes: 4 additions & 0 deletions .github/workflows/production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ jobs:
GITBOOK_ENDPOINT: https://api.gitbook.com
GITBOOK_ORGANIZATION: gitbook
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
# Proxy
GITBOOK_PROXY_URL: ${{ secrets.PROXY_URL }}
GITBOOK_PROXY_SECRET: ${{ secrets.PROXY_SECRET }}
REFLAG_SECRET_KEY: ${{ secrets.REFLAG_SECRET_KEY }}
# GitHub Files
UNFURL_GITHUB_CLIENT_ID: ${{ secrets.UNFURL_GITHUB_CLIENT_ID }}
UNFURL_GITHUB_CLIENT_SECRET: ${{ secrets.UNFURL_GITHUB_CLIENT_SECRET }}
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ jobs:
GITBOOK_ENDPOINT: https://api.gitbook-staging.com
GITBOOK_ORGANIZATION: gitbookio
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
# Proxy
GITBOOK_PROXY_URL: ${{ secrets.PROXY_STAGING_URL }}
GITBOOK_PROXY_SECRET: ${{ secrets.PROXY_STAGING_SECRET }}
REFLAG_SECRET_KEY: ${{ secrets.REFLAG_STAGING_SECRET_KEY }}
# GitHub Files
UNFURL_GITHUB_CLIENT_ID: ${{ secrets.UNFURL_GITHUB_CLIENT_ID }}
UNFURL_GITHUB_CLIENT_SECRET: ${{ secrets.UNFURL_GITHUB_CLIENT_SECRET }}
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ jobs:
run: bun run check
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
# Proxy
GITBOOK_PROXY_URL: ${{ secrets.PROXY_STAGING_URL }}
GITBOOK_PROXY_SECRET: ${{ secrets.PROXY_STAGING_SECRET }}
REFLAG_SECRET_KEY: ${{ secrets.REFLAG_STAGING_SECRET_KEY }}
# GitHub Files
UNFURL_GITHUB_CLIENT_ID: ${{ secrets.UNFURL_GITHUB_CLIENT_ID }}
UNFURL_GITHUB_CLIENT_SECRET: ${{ secrets.UNFURL_GITHUB_CLIENT_SECRET }}
Expand Down
6 changes: 3 additions & 3 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@
},
"integrations/intercom-conversations": {
"name": "@gitbook/integration-intercom-conversations",
"version": "0.1.0",
"version": "0.2.0",
"dependencies": {
"@gitbook/api": "*",
"@gitbook/runtime": "*",
Expand Down Expand Up @@ -726,7 +726,7 @@
},
"packages/api": {
"name": "@gitbook/api",
"version": "0.145.0",
"version": "0.147.0",
"dependencies": {
"event-iterator": "^2.0.0",
"eventsource-parser": "^3.0.0",
Expand All @@ -741,7 +741,7 @@
},
"packages/cli": {
"name": "@gitbook/cli",
"version": "0.26.0",
"version": "0.26.2",
"bin": {
"gitbook": "./cli.js",
},
Expand Down
4 changes: 4 additions & 0 deletions integrations/gitlab/gitbook-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,8 @@ scopes:
configurations:
space:
componentId: configure
secrets:
PROXY_URL: ${{ env.GITBOOK_PROXY_URL }}
PROXY_SECRET: ${{ env.GITBOOK_PROXY_SECRET }}
REFLAG_SECRET_KEY: ${{ env.REFLAG_SECRET_KEY }}
target: space
128 changes: 104 additions & 24 deletions integrations/gitlab/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import LinkHeader from 'http-link-header';

import { Logger, ExposableError } from '@gitbook/runtime';

import type { GitLabSpaceConfiguration } from './types';
import type { GitLabRuntimeContext, GitLabSpaceConfiguration } from './types';
import { signResponse } from './utils';

const logger = Logger('gitlab:api');

Expand Down Expand Up @@ -38,8 +39,11 @@ interface GLFetchOptions {
/**
* Fetch the current GitLab user. It will use the access token from the environment.
*/
export async function getCurrentUser(config: GitLabSpaceConfiguration) {
const user = await gitlabAPI<GLUser>(config, {
export async function getCurrentUser(
context: GitLabRuntimeContext,
config: GitLabSpaceConfiguration,
) {
const user = await gitlabAPI<GLUser>(context, config, {
path: '/user',
});

Expand All @@ -51,10 +55,11 @@ export async function getCurrentUser(config: GitLabSpaceConfiguration) {
* the access token from the environment.
*/
export async function fetchProjects(
context: GitLabRuntimeContext,
config: GitLabSpaceConfiguration,
options: GLFetchOptions = {},
) {
const projects = await gitlabAPI<Array<GLProject>>(config, {
const projects = await gitlabAPI<Array<GLProject>>(context, config, {
path: '/projects',
params: {
membership: true,
Expand All @@ -71,11 +76,12 @@ export async function fetchProjects(
* Search currently authenticated user projects for a given query.
*/
export async function searchUserProjects(
context: GitLabRuntimeContext,
config: GitLabSpaceConfiguration,
search: string,
options: GLFetchOptions = {},
) {
const projects = await gitlabAPI<Array<GLProject>>(config, {
const projects = await gitlabAPI<Array<GLProject>>(context, config, {
path: `/users/${config.userId}/projects`,
params: {
search,
Expand All @@ -91,8 +97,12 @@ export async function searchUserProjects(
/**
* Fetch a GitLab project by its ID.
*/
export async function fetchProject(config: GitLabSpaceConfiguration, projectId: number) {
const project = await gitlabAPI<GLProject>(config, {
export async function fetchProject(
context: GitLabRuntimeContext,
config: GitLabSpaceConfiguration,
projectId: number,
) {
const project = await gitlabAPI<GLProject>(context, config, {
path: `/projects/${projectId}`,
});

Expand All @@ -102,8 +112,12 @@ export async function fetchProject(config: GitLabSpaceConfiguration, projectId:
/**
* Fetch all branches for a given project repository.
*/
export async function fetchProjectBranches(config: GitLabSpaceConfiguration, projectId: number) {
const branches = await gitlabAPI<Array<GLBranch>>(config, {
export async function fetchProjectBranches(
context: GitLabRuntimeContext,
config: GitLabSpaceConfiguration,
projectId: number,
) {
const branches = await gitlabAPI<Array<GLBranch>>(context, config, {
path: `/projects/${projectId}/repository/branches`,
params: {
per_page: 100,
Expand All @@ -118,12 +132,13 @@ export async function fetchProjectBranches(config: GitLabSpaceConfiguration, pro
* Configure a GitLab webhook for a given project.
*/
export async function addProjectWebhook(
context: GitLabRuntimeContext,
config: GitLabSpaceConfiguration,
projectId: number,
webhookUrl: string,
webhookToken: string,
) {
const { id } = await gitlabAPI<{ id: number }>(config, {
const { id } = await gitlabAPI<{ id: number }>(context, config, {
method: 'POST',
path: `/projects/${projectId}/hooks`,
body: {
Expand All @@ -141,11 +156,12 @@ export async function addProjectWebhook(
* Delete a GitLab webhook for a given project.
*/
export async function deleteProjectWebhook(
context: GitLabRuntimeContext,
config: GitLabSpaceConfiguration,
projectId: number,
webhookId: number,
) {
await gitlabAPI(config, {
await gitlabAPI(context, config, {
method: 'DELETE',
path: `/projects/${projectId}/hooks/${webhookId}`,
});
Expand All @@ -155,12 +171,13 @@ export async function deleteProjectWebhook(
* Create a commit status for a commit SHA.
*/
export async function editCommitStatus(
context: GitLabRuntimeContext,
config: GitLabSpaceConfiguration,
projectId: number,
sha: string,
status: object,
): Promise<void> {
await gitlabAPI(config, {
await gitlabAPI(context, config, {
method: 'POST',
path: `/projects/${projectId}/statuses/${sha}`,
body: status,
Expand All @@ -171,6 +188,7 @@ export async function editCommitStatus(
* Execute a GitLab API request.
*/
export async function gitlabAPI<T>(
context: GitLabRuntimeContext,
config: GitLabSpaceConfiguration,
request: {
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
Expand Down Expand Up @@ -209,7 +227,7 @@ export async function gitlabAPI<T>(
body: body ? JSON.stringify(body) : undefined,
};

const response = await requestGitLab(token, url, options);
const response = await requestGitLab(context, token, url, options);

const isJSONResponse = response.headers.get('Content-Type')?.includes('application/json');
if (!isJSONResponse) {
Expand All @@ -234,7 +252,7 @@ export async function gitlabAPI<T>(
const nextURLSearchParams = Object.fromEntries(nextURL.searchParams);
if (nextURLSearchParams.page) {
url.searchParams.set('page', nextURLSearchParams.page as string);
const nextResponse = await requestGitLab(token, url, options);
const nextResponse = await requestGitLab(context, token, url, options);
const nextData = await nextResponse.json();
// @ts-ignore
data = [...data, ...(paginatedListProperty ? nextData[listProperty] : nextData)];
Expand All @@ -253,21 +271,35 @@ export async function gitlabAPI<T>(
* It will throw an error if the response is not ok.
*/
async function requestGitLab(
context: GitLabRuntimeContext,
token: string,
url: URL,
options: RequestInit = {},
): Promise<Response> {
logger.debug(`GitLab API -> [${options.method}] ${url.toString()}`);
const response = await fetch(url.toString(), {
...options,
headers: {
...options.headers,
...(options.body ? { 'Content-Type': 'application/json' } : {}),
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'GitLab-Integration-Worker',
},
});
// Hardcoded test org, will need to switch to use Reflag for that.
const useProxy = await shouldUseProxy(context);
const response = useProxy
? await proxyRequest(context, url.toString(), {
...options,
headers: {
...options.headers,
...(options.body ? { 'Content-Type': 'application/json' } : {}),
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'GitLab-Integration-Worker',
},
})
: await fetch(url.toString(), {
...options,
headers: {
...options.headers,
...(options.body ? { 'Content-Type': 'application/json' } : {}),
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'GitLab-Integration-Worker',
},
});

if (!response.ok) {
const text = await response.text();
Expand Down Expand Up @@ -303,3 +335,51 @@ export function getAccessTokenOrThrow(config: GitLabSpaceConfiguration): string

return accessToken;
}

export async function proxyRequest(
context: GitLabRuntimeContext,
url: string,
options: RequestInit = {},
): Promise<Response> {
const signature = await signResponse(url, context.environment.secrets.PROXY_SECRET);
const proxyUrl = new URL(context.environment.secrets.PROXY_URL);

proxyUrl.searchParams.set('target', url);

return fetch(proxyUrl.toString(), {
...options,
headers: {
...options.headers,
'X-Gitbook-Proxy-Signature': signature,
},
});
}

export async function shouldUseProxy(context: GitLabRuntimeContext): Promise<boolean> {
const companyId = context.environment.installation?.target.organization;
if (!companyId) {
return false;
}
try {
const response = await fetch(
`https://front.reflag.com/features/enabled?context.company.id=${companyId}&key=GIT_SYNC_STATIC_IP`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${context.environment.secrets.REFLAG_SECRET_KEY}`,
'Content-Type': 'application/json',
},
},
);

const json = (await response.json()) as {
features: { GIT_SYNC_STATIC_IP: { isEnabled: boolean } };
};
const flag = json.features.GIT_SYNC_STATIC_IP;

return flag.isEnabled;
} catch (e) {
logger.error('Error checking Reflag feature flag:', e);
return false;
}
}
4 changes: 2 additions & 2 deletions integrations/gitlab/src/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const configBlock = createComponent<
: undefined,
};

const glUser = await getCurrentUser(config);
const glUser = await getCurrentUser(context, config);

await context.api.integrations.updateIntegrationSpaceInstallation(
spaceInstallation.integration,
Expand Down Expand Up @@ -149,7 +149,7 @@ export const configBlock = createComponent<

case 'save.configuration': {
await saveSpaceConfiguration(context, element.state);
return { type: 'complete' };
return { type: 'complete' as const };
}
}
},
Expand Down
9 changes: 5 additions & 4 deletions integrations/gitlab/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ const handleFetchEvent: FetchEventCallback<GitLabRuntimeContext> = async (reques
if (querySelectedProject) {
try {
const selectedProject = await fetchProject(
context,
spaceConfig,
parseInt(querySelectedProject, 10),
);
Expand All @@ -180,7 +181,7 @@ const handleFetchEvent: FetchEventCallback<GitLabRuntimeContext> = async (reques

if (queryProject) {
const q = encodeURIComponent(queryProject);
const searchedProjects = await searchUserProjects(spaceConfig, q, {
const searchedProjects = await searchUserProjects(context, spaceConfig, q, {
page: 1,
per_page: 100,
walkPagination: false,
Expand All @@ -201,7 +202,7 @@ const handleFetchEvent: FetchEventCallback<GitLabRuntimeContext> = async (reques
});
} else {
const page = pageNumber || 1;
const projects = await fetchProjects(spaceConfig, {
const projects = await fetchProjects(context, spaceConfig, {
page,
per_page: 100,
walkPagination: false,
Expand Down Expand Up @@ -251,7 +252,7 @@ const handleFetchEvent: FetchEventCallback<GitLabRuntimeContext> = async (reques
const querySelectedBranch =
selectedBranch && typeof selectedBranch === 'string' ? selectedBranch : undefined;

const branches = projectId ? await fetchProjectBranches(config, projectId) : [];
const branches = projectId ? await fetchProjectBranches(context, config, projectId) : [];

const data = branches.map(
(branch): ContentKitSelectOption => ({
Expand Down Expand Up @@ -405,7 +406,7 @@ const handleSpaceInstallationDeleted: EventCallback<
return;
}

await uninstallWebhook(configuration);
await uninstallWebhook(context, configuration);
};

export default createIntegration({
Expand Down
Loading
Loading