Files
triggerdotdev--trigger.dev/integrations/github/src/sources.ts
T
Eric Allam c0dfa8048a feat: BYO Auth (#491)
* feat: BYO Auth

Define client-side auth resolvers to be able to supply custom authentication credentials for integrations before a run is performed

- Added new defineAuthResolver
- Update all integrations to support the new auth resolvers
- Strip internal symbols from .d.ts in integrations and trigger-sdk
- Added BYO Auth docs
- Update Dynamic Schedule to support associated account IDs
- Create external accounts just-in-time
- Added Account ID field to test job when there are external auth integrations
- Show Account ID on run dashboard
- Added new Run error state called “Unresolved auth”

* Added changeset

* Remove @internal from TriggerIntegration public methods

* Add void to the result union

* DynamicTriggers now work with the new BYO auth system, and added a bunch of docs and docs changes

* Add additional key material for registering dynamic trigger task

* Add new define* instance methods to the overview
2023-09-22 08:54:31 -07:00

296 lines
7.7 KiB
TypeScript

import { Webhooks } from "@octokit/webhooks";
import { omit, safeJsonParse } from "@trigger.dev/integration-kit";
import type { Logger } from "@trigger.dev/sdk";
import { ExternalSource, HandlerEvent } from "@trigger.dev/sdk";
import { z } from "zod";
import { Github } from "./index";
type WebhookData = {
id: number;
active: boolean;
events: string[];
config: {
url: string;
};
};
function webhookData(data: any): data is WebhookData {
return (
typeof data === "object" &&
data !== null &&
typeof data.id === "number" &&
typeof data.config === "object"
);
}
export function createRepoEventSource(
integration: Github
): ExternalSource<Github, { owner: string; repo: string }, "HTTP", {}> {
return new ExternalSource("HTTP", {
id: "github.repo",
version: "0.1.1",
schema: z.object({ owner: z.string(), repo: z.string() }),
integration,
key: (params) => `${params.owner}/${params.repo}`,
properties: (params) => [
{
label: "Owner",
text: params.owner,
url: `https://github.com/${params.owner}`,
},
{
label: "Repo",
text: params.repo,
url: `https://github.com/${params.owner}/${params.repo}`,
},
],
filter: (params) => ({
repository: {
full_name: [`${params.owner}/${params.repo}`],
},
}),
handler: webhookHandler,
register: async (event, io, ctx) => {
const { params, source: httpSource, options } = event;
const registeredOptions = {
event: options.event.desired,
};
if (httpSource.active && webhookData(httpSource.data)) {
const hasMissingOptions = Object.values(options).some(
(option) => option.missing.length > 0
);
if (!hasMissingOptions) return;
// We need to update the webhook to add the new events and then return
const newWebhookData = await io.integration.updateWebhook("update-webhook", {
owner: params.owner,
repo: params.repo,
hookId: httpSource.data.id,
url: httpSource.url,
secret: httpSource.secret,
addEvents: options.event.missing,
});
return {
data: newWebhookData,
options: registeredOptions,
};
}
const webhooks = await io.integration.listWebhooks("list-webhooks", {
owner: params.owner,
repo: params.repo,
});
const existingWebhook = webhooks.find((w) => w.config.url === httpSource.url);
// There is an existing webhook, but it's not synced with Trigger.dev, so we need to update it with the secret
if (existingWebhook && existingWebhook.active) {
const updatedWebhook = await io.integration.updateWebhook("update-webhook", {
owner: params.owner,
repo: params.repo,
hookId: existingWebhook.id,
url: httpSource.url,
secret: httpSource.secret,
addEvents: options.event.missing,
});
return {
data: updatedWebhook,
options: registeredOptions,
};
}
const webhook = await io.integration.createWebhook("create-webhook", {
owner: params.owner,
repo: params.repo,
events: options.event.desired,
url: httpSource.url,
secret: httpSource.secret,
});
return { data: webhook, options: registeredOptions };
},
});
}
export function createOrgEventSource(
integration: Github
): ExternalSource<Github, { org: string }, "HTTP", {}> {
return new ExternalSource("HTTP", {
id: "github.org",
version: "0.1.1",
integration,
schema: z.object({ org: z.string() }),
key: (params) => params.org,
properties: (params) => [
{
label: "Org",
text: params.org,
url: `https://github.com/${params.org}`,
},
],
filter: (params) => ({
organization: {
login: [params.org],
},
}),
handler: webhookHandler,
register: async (event, io, ctx) => {
const { params, source: httpSource, options } = event;
const registeredOptions = {
event: options.event.desired,
};
const hasMissingOptions = Object.values(options).some((option) => option.missing.length > 0);
if (
httpSource.active &&
webhookData(httpSource.data) &&
httpSource.secret &&
hasMissingOptions
) {
const existingData = httpSource.data;
// We need to update the webhook to add the new events and then return
const newWebhookData = await io.integration.updateOrgWebhook("update-webhook", {
org: params.org,
hookId: existingData.id,
url: httpSource.url,
secret: httpSource.secret,
addEvents: options.event.missing,
});
return {
secret: httpSource.secret,
data: newWebhookData,
options: registeredOptions,
};
}
const webhooks = await io.integration.listOrgWebhooks("list-webhooks", {
org: params.org,
});
const existingWebhook = webhooks.find((w) => w.config.url === httpSource.url);
const secret = Math.random().toString(36).slice(2);
if (existingWebhook && existingWebhook.active) {
const updatedWebhook = await io.integration.updateOrgWebhook("update-webhook", {
org: params.org,
hookId: existingWebhook.id,
url: httpSource.url,
secret,
});
return {
secret,
data: updatedWebhook,
options: registeredOptions,
};
}
const webhook = await io.integration.createOrgWebhook("create-webhook", {
org: params.org,
events: options.event.desired,
url: httpSource.url,
secret,
});
return { secret, data: webhook, options: registeredOptions };
},
});
}
// Parses the body of a request
// If it's a Buffer, it will be parsed as JSON
function parseBody(body: any) {
if (Buffer.isBuffer(body)) {
return safeJsonParse(body.toString());
}
if (typeof body === "string") {
return safeJsonParse(body);
}
return body;
}
async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger) {
logger.debug("[inside github integration] Handling github repo event");
const { rawEvent: request, source } = event;
if (!request.body) {
logger.debug("[inside github integration] No body found");
return;
}
const rawBody = await request.text();
const deliveryId = request.headers.get("x-github-delivery");
const hookId = request.headers.get("x-github-hook-id");
const signature = request.headers.get("x-hub-signature-256");
if (source.secret && signature) {
const githubWebhooks = new Webhooks({
secret: source.secret,
});
if (!githubWebhooks.verify(rawBody, signature)) {
logger.debug("[inside github integration] Unable to verify the signature of the rawBody", {
signature,
secret: source.secret,
});
return;
}
}
const name = request.headers.get("x-github-event") ?? "unknown";
const allHeaders = Object.fromEntries(request.headers.entries());
const context = omit(allHeaders, [
"x-github-event",
"x-github-delivery",
"x-hub-signature-256",
"x-hub-signature",
"content-type",
"content-length",
"accept",
"accept-encoding",
"x-forwarded-proto",
]);
const payload = parseBody(rawBody);
if (!payload) {
logger.debug("[inside github integration] Unable to parse the rawBody");
return;
}
logger.debug("[inside github integration] Returning an event for the webhook!", {
name,
payload,
context,
});
return {
events: [
{
id: [hookId, deliveryId].join(":"),
source: "github.com",
payload,
name,
context,
},
],
};
}