Skip to content

Commit ef13f61

Browse files
Bucket integration (#807)
* WIP: Bucket integration * scripts * bun lock * format * WIP * target: site * use unsigned instead of public * use unsigned instead of public * bun lock * fix lock * undo vscode settings change * update bun lock * Use component for Bucket integration configuration flow (#837) Use component for Bucket configuration flow * Use renamed adaptive schema endpoint (#841) * fix bun lock * bun lock * update package lock * assets * changeset --------- Co-authored-by: spastorelli <[email protected]>
1 parent 71feef5 commit ef13f61

File tree

12 files changed

+569
-1
lines changed

12 files changed

+569
-1
lines changed

.changeset/smart-candies-scream.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/integration-bucket': minor
3+
---
4+
5+
Initial version

bun.lock

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@
2626
"@gitbook/tsconfig": "workspace:*",
2727
},
2828
},
29+
"integrations/bucket": {
30+
"name": "@gitbook/integration-bucket",
31+
"dependencies": {
32+
"@gitbook/runtime": "*",
33+
"itty-router": "^5.0.18",
34+
},
35+
"devDependencies": {
36+
"@cloudflare/workers-types": "*",
37+
"@gitbook/cli": "workspace:*",
38+
"@gitbook/tsconfig": "workspace:*",
39+
},
40+
},
2941
"integrations/cognito": {
3042
"name": "@gitbook/integration-cognito",
3143
"version": "0.3.0",
@@ -903,6 +915,8 @@
903915

904916
"@gitbook/integration-arcade": ["@gitbook/integration-arcade@workspace:integrations/arcade"],
905917

918+
"@gitbook/integration-bucket": ["@gitbook/integration-bucket@workspace:integrations/bucket"],
919+
906920
"@gitbook/integration-cognito": ["@gitbook/integration-cognito@workspace:integrations/cognito"],
907921

908922
"@gitbook/integration-crisp": ["@gitbook/integration-crisp@workspace:integrations/crisp"],
@@ -1831,7 +1845,7 @@
18311845

18321846
"isomorphic-ws": ["[email protected]", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
18331847

1834-
"itty-router": ["itty-router@4.2.2", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
1848+
"itty-router": ["itty-router@5.0.18", "", {}, "sha512-mK3ReOt4ARAGy0V0J7uHmArG2USN2x0zprZ+u+YgmeRjXTDbaowDy3kPcsmQY6tH+uHhDgpWit9Vqmv/4rTXwA=="],
18351849

18361850
"jiti": ["[email protected]", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
18371851

@@ -2453,22 +2467,42 @@
24532467

24542468
"@gitbook/cli/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="],
24552469

2470+
"@gitbook/integration-cognito/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
2471+
2472+
"@gitbook/integration-discord/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
2473+
2474+
"@gitbook/integration-github/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
2475+
24562476
"@gitbook/integration-github-copilot/@tsndr/cloudflare-worker-jwt": ["@tsndr/[email protected]", "", {}, "sha512-g1jSm5olPqKh15kadnj0666YPudibHYGyFyM0URLXSeY5MzNIGkfhFedLgKHq8NCDBMzLUMX7Oz8d+jmQXqBuw=="],
24572477

2478+
"@gitbook/integration-github-copilot/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
2479+
24582480
"@gitbook/integration-github-files/itty-router": ["[email protected]", "", {}, "sha512-s98XTPhle6GGbaFf0kYrOD3Q8gyhnqvOqkwYijC3AmkceNKqWUp13YHg6dWmqmVv4pP7l7c94XI92I0EXVGO0w=="],
24592481

2482+
"@gitbook/integration-gitlab/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
2483+
24602484
"@gitbook/integration-jira/itty-router": ["[email protected]", "", {}, "sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g=="],
24612485

2486+
"@gitbook/integration-lucid/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
2487+
24622488
"@gitbook/integration-mailchimp/itty-router": ["[email protected]", "", {}, "sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g=="],
24632489

24642490
"@gitbook/integration-oidc/@tsndr/cloudflare-worker-jwt": ["@tsndr/[email protected]", "", {}, "sha512-g1jSm5olPqKh15kadnj0666YPudibHYGyFyM0URLXSeY5MzNIGkfhFedLgKHq8NCDBMzLUMX7Oz8d+jmQXqBuw=="],
24652491

2492+
"@gitbook/integration-oidc/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
2493+
24662494
"@gitbook/integration-runkit/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/android-arm": "0.15.18", "@esbuild/linux-loong64": "0.15.18", "esbuild-android-64": "0.15.18", "esbuild-android-arm64": "0.15.18", "esbuild-darwin-64": "0.15.18", "esbuild-darwin-arm64": "0.15.18", "esbuild-freebsd-64": "0.15.18", "esbuild-freebsd-arm64": "0.15.18", "esbuild-linux-32": "0.15.18", "esbuild-linux-64": "0.15.18", "esbuild-linux-arm": "0.15.18", "esbuild-linux-arm64": "0.15.18", "esbuild-linux-mips64le": "0.15.18", "esbuild-linux-ppc64le": "0.15.18", "esbuild-linux-riscv64": "0.15.18", "esbuild-linux-s390x": "0.15.18", "esbuild-netbsd-64": "0.15.18", "esbuild-openbsd-64": "0.15.18", "esbuild-sunos-64": "0.15.18", "esbuild-windows-32": "0.15.18", "esbuild-windows-64": "0.15.18", "esbuild-windows-arm64": "0.15.18" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q=="],
24672495

24682496
"@gitbook/integration-runkit/itty-router": ["[email protected]", "", {}, "sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g=="],
24692497

24702498
"@gitbook/integration-slack/itty-router": ["[email protected]", "", {}, "sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g=="],
24712499

2500+
"@gitbook/integration-va-auth0/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
2501+
2502+
"@gitbook/integration-va-azure/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
2503+
2504+
"@gitbook/integration-va-okta/itty-router": ["[email protected]", "", {}, "sha512-KegPW0l9SNPadProoFT07AB84uOqLUwzlXQ7HsqkS31WUrxkjdhcemRpTDUuetbMJ89uBtWeQSVoiEmUAu31uw=="],
2505+
24722506
"@gitbook/runtime/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/linux-loong64": "0.14.54", "esbuild-android-64": "0.14.54", "esbuild-android-arm64": "0.14.54", "esbuild-darwin-64": "0.14.54", "esbuild-darwin-arm64": "0.14.54", "esbuild-freebsd-64": "0.14.54", "esbuild-freebsd-arm64": "0.14.54", "esbuild-linux-32": "0.14.54", "esbuild-linux-64": "0.14.54", "esbuild-linux-arm": "0.14.54", "esbuild-linux-arm64": "0.14.54", "esbuild-linux-mips64le": "0.14.54", "esbuild-linux-ppc64le": "0.14.54", "esbuild-linux-riscv64": "0.14.54", "esbuild-linux-s390x": "0.14.54", "esbuild-netbsd-64": "0.14.54", "esbuild-openbsd-64": "0.14.54", "esbuild-sunos-64": "0.14.54", "esbuild-windows-32": "0.14.54", "esbuild-windows-64": "0.14.54", "esbuild-windows-arm64": "0.14.54" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA=="],
24732507

24742508
"@graphql-codegen/cli/chalk": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],

integrations/bucket/assets/icon.png

3.46 KB
Loading
47.9 KB
Loading
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: bucket
2+
title: Bucket
3+
script: src/index.tsx
4+
description: Sync your GitBook site’s schema with Bucket to deliver adaptive experiences to users.
5+
summary: |
6+
# Overview
7+
8+
This integration allows you to sync your GitBook site’s schema with feature flags from Bucket in order to bring user data into content allowing you to create adaptive content experiences.
9+
10+
# How it works
11+
12+
The integration automatically synchronizes your GitBook site schema with the feature flags defined in Bucket. Any changes made in Bucket will be reflected in your site's schema.
13+
14+
# Configure
15+
16+
To configure the integration, you need to connect your GitBook site to your Bucket account by providing your Bucket secret key. Once connected, you can manage your feature flags in Bucket, and they will be automatically reflected in your GitBook site schema.
17+
icon: ./assets/icon.png
18+
previewImages:
19+
- ./assets/preview.png
20+
scopes:
21+
- site:adaptive:read
22+
- site:adaptive:write
23+
visibility: private
24+
organization: gitbook
25+
configurations:
26+
site:
27+
componentId: config
28+
secrets: {}
29+
target: site

integrations/bucket/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@gitbook/integration-bucket",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"typecheck": "tsc --noEmit",
7+
"publish-integrations-staging": "gitbook publish .",
8+
"check": "gitbook check",
9+
"publish-integrations": "gitbook publish ."
10+
},
11+
"dependencies": {
12+
"@gitbook/runtime": "*",
13+
"itty-router": "^5.0.18"
14+
},
15+
"devDependencies": {
16+
"@gitbook/cli": "workspace:*",
17+
"@gitbook/tsconfig": "workspace:*",
18+
"@cloudflare/workers-types": "*"
19+
}
20+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { createComponent, ExposableError } from '@gitbook/runtime';
2+
import {
3+
BucketAction,
4+
BucketProps,
5+
BucketRuntimeContext,
6+
BucketSiteInstallationConfiguration,
7+
BucketState,
8+
} from './types';
9+
import { assertSiteInstallation } from './utils';
10+
11+
export const configBlock = createComponent<
12+
BucketProps,
13+
BucketState,
14+
BucketAction,
15+
BucketRuntimeContext
16+
>({
17+
componentId: 'config',
18+
initialState: (props) => {
19+
const siteInstallation = props.siteInstallation;
20+
return {
21+
secret_key: siteInstallation.configuration?.secret_key || '',
22+
};
23+
},
24+
action: async (element, action, context) => {
25+
switch (action.action) {
26+
case 'save.config':
27+
const { api, environment } = context;
28+
29+
const siteInstallation = assertSiteInstallation(environment);
30+
31+
const secretKey = element.state.secret_key;
32+
if (typeof secretKey !== 'string' || !secretKey) {
33+
throw new ExposableError(
34+
'Incomplete configuration: missing Bucket environment secret key',
35+
);
36+
}
37+
38+
const configurationBody: BucketSiteInstallationConfiguration = {
39+
secret_key: secretKey,
40+
};
41+
42+
await api.integrations.updateIntegrationSiteInstallation(
43+
siteInstallation.integration,
44+
siteInstallation.installation,
45+
siteInstallation.site,
46+
{
47+
configuration: {
48+
...configurationBody,
49+
},
50+
},
51+
);
52+
53+
return { type: 'complete' };
54+
}
55+
},
56+
render: async () => {
57+
return (
58+
<block>
59+
<input
60+
label="Secret key"
61+
hint={
62+
<text>
63+
The secret key from your Bucket{' '}
64+
<link
65+
target={{
66+
url: 'https://app.bucket.co/envs/current/settings/app-environments',
67+
}}
68+
>
69+
environment settings.
70+
</link>
71+
</text>
72+
}
73+
element={<textinput state="secret_key" placeholder="Secret key" />}
74+
/>
75+
<input
76+
label=""
77+
hint=""
78+
element={
79+
<button
80+
style="primary"
81+
disabled={false}
82+
label="Save"
83+
tooltip="Save configuration"
84+
onPress={{
85+
action: 'save.config',
86+
}}
87+
/>
88+
}
89+
/>
90+
</block>
91+
);
92+
},
93+
});

integrations/bucket/src/index.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { GitBookAPI } from '@gitbook/api';
2+
import { Router } from 'itty-router';
3+
import {
4+
createIntegration,
5+
ExposableError,
6+
Logger,
7+
type FetchEventCallback,
8+
} from '@gitbook/runtime';
9+
import { arrayToHex, assertSiteInstallation, safeCompare } from './utils';
10+
import { type BucketRuntimeContext, type IntegrationTask } from './types';
11+
import { handleIntegrationTask } from './tasks';
12+
import { configBlock } from './components';
13+
14+
const logger = Logger('bucket');
15+
16+
const handleFetchEvent: FetchEventCallback<BucketRuntimeContext> = async (request, context) => {
17+
const { environment } = context;
18+
19+
const siteInstallation = assertSiteInstallation(environment);
20+
21+
const router = Router({
22+
base: new URL(
23+
siteInstallation.urls?.publicEndpoint ||
24+
environment.installation?.urls.publicEndpoint ||
25+
environment.integration.urls.publicEndpoint,
26+
).pathname,
27+
});
28+
29+
async function verifyIntegrationSignature(
30+
payload: string,
31+
signature: string,
32+
secret: string,
33+
): Promise<boolean> {
34+
if (!signature) {
35+
return false;
36+
}
37+
38+
const algorithm = { name: 'HMAC', hash: 'SHA-256' };
39+
const enc = new TextEncoder();
40+
const key = await crypto.subtle.importKey('raw', enc.encode(secret), algorithm, false, [
41+
'sign',
42+
'verify',
43+
]);
44+
const signed = await crypto.subtle.sign(algorithm.name, key, enc.encode(payload));
45+
const expectedSignature = arrayToHex(signed);
46+
47+
return safeCompare(expectedSignature, signature);
48+
}
49+
50+
/**
51+
* Handle integration tasks
52+
*/
53+
router.post('/tasks', async (request) => {
54+
const signature = request.headers.get('x-gitbook-integration-signature') ?? '';
55+
const payloadString = await request.text();
56+
57+
const verified = await verifyIntegrationSignature(
58+
payloadString,
59+
signature,
60+
environment.signingSecrets.integration,
61+
);
62+
63+
if (!verified) {
64+
const message = 'Invalid signature for integration task';
65+
logger.error(message);
66+
throw new ExposableError(message);
67+
}
68+
69+
const { task } = JSON.parse(payloadString) as { task: IntegrationTask };
70+
logger.debug('verified & received integration task', task);
71+
72+
context.waitUntil(
73+
(async () => {
74+
await handleIntegrationTask(context, task);
75+
})(),
76+
);
77+
78+
return new Response(JSON.stringify({ acknowledged: true }), {
79+
status: 200,
80+
headers: { 'content-type': 'application/json' },
81+
});
82+
});
83+
84+
const response = (await router.handle(request, context)) as Response | undefined;
85+
86+
if (!response) {
87+
return new Response(`No route matching ${request.method} ${request.url}`, {
88+
status: 404,
89+
});
90+
}
91+
92+
return response;
93+
};
94+
95+
export default createIntegration({
96+
fetch: handleFetchEvent,
97+
components: [configBlock],
98+
events: {
99+
site_installation_setup: async (event, context) => {
100+
const api = new GitBookAPI({
101+
authToken: context.environment.apiTokens.integration,
102+
endpoint: context.environment.apiEndpoint,
103+
userAgent: context.api.userAgent,
104+
});
105+
106+
const organizationId = context.environment.installation?.target.organization;
107+
if (!organizationId) {
108+
throw new Error(
109+
`No organization ID found in the installation ${event.installationId}`,
110+
);
111+
}
112+
113+
// Sync the adaptive schema for the site and queue a task
114+
// to sync it every hour
115+
await handleIntegrationTask(
116+
{
117+
...context,
118+
api,
119+
},
120+
{
121+
type: 'sync-adaptive-schema',
122+
payload: {
123+
siteId: event.siteId,
124+
installationId: event.installationId,
125+
organizationId,
126+
},
127+
},
128+
);
129+
},
130+
},
131+
});

0 commit comments

Comments
 (0)